ILIAS  trunk Revision v11.0_alpha-1846-g895b5f47236
All Data Structures Namespaces Files Functions Variables Enumerations Enumerator Modules Pages
Renderer.php
Go to the documentation of this file.
1 <?php
2 
19 declare(strict_types=1);
20 
22 
32 use LogicException;
33 use Closure;
40 
46 {
47  public const DATETIME_DATEPICKER_MINMAX_FORMAT = 'Y-m-d\Th:m';
48  public const DATE_DATEPICKER_MINMAX_FORMAT = 'Y-m-d';
49  public const TYPE_DATE = 'date';
50  public const TYPE_DATETIME = 'datetime-local';
51  public const TYPE_TIME = 'time';
52  public const HTML5_NATIVE_DATETIME_FORMAT = 'Y-m-d H:i';
53  public const HTML5_NATIVE_DATE_FORMAT = 'Y-m-d';
54  public const HTML5_NATIVE_TIME_FORMAT = 'H:i';
55 
56  public const DATEPICKER_FORMAT_MAPPING = [
57  'd' => 'DD',
58  'jS' => 'Do',
59  'l' => 'dddd',
60  'D' => 'dd',
61  'S' => 'o',
62  'i' => 'mm',
63  'W' => '',
64  'm' => 'MM',
65  'F' => 'MMMM',
66  'M' => 'MMM',
67  'Y' => 'YYYY',
68  'y' => 'YY'
69  ];
70 
77  protected const FILE_UPLOAD_CHUNK_SIZE_FACTOR = 0.9;
78 
79  private const CENTUM = 100;
80 
84  public function render(Component\Component $component, RendererInterface $default_renderer): string
85  {
86  $component = $this->setSignals($component);
87 
88  switch (true) {
89  case ($component instanceof F\OptionalGroup):
90  return $this->renderOptionalGroup($component, $default_renderer);
91 
92  case ($component instanceof F\SwitchableGroup):
93  return $this->renderSwitchableGroup($component, $default_renderer);
94 
95  case ($component instanceof F\Section):
96  return $this->renderSection($component, $default_renderer);
97 
98  case ($component instanceof F\Duration):
99  return $this->renderDurationField($component, $default_renderer);
100 
101  case ($component instanceof F\Link):
102  return $this->renderLinkField($component, $default_renderer);
103 
104  case ($component instanceof F\Group):
105  return $default_renderer->render($component->getInputs());
106 
107  case ($component instanceof F\Text):
108  return $this->renderTextField($component, $default_renderer);
109 
110  case ($component instanceof F\Numeric):
111  return $this->renderNumericField($component, $default_renderer);
112 
113  case ($component instanceof F\Checkbox):
114  return $this->renderCheckboxField($component, $default_renderer);
115 
116  case ($component instanceof F\Tag):
117  return $this->renderTagField($component, $default_renderer);
118 
119  case ($component instanceof F\Password):
120  return $this->renderPasswordField($component, $default_renderer);
121 
122  case ($component instanceof F\Select):
123  return $this->renderSelectField($component, $default_renderer);
124 
125  case ($component instanceof F\Markdown):
126  return $this->renderMarkdownField($component, $default_renderer);
127 
128  case ($component instanceof F\Textarea):
129  return $this->renderTextareaField($component, $default_renderer);
130 
131  case ($component instanceof F\Radio):
132  return $this->renderRadioField($component, $default_renderer);
133 
134  case ($component instanceof F\MultiSelect):
135  return $this->renderMultiSelectField($component, $default_renderer);
136 
137  case ($component instanceof F\DateTime):
138  return $this->renderDateTimeField($component, $default_renderer);
139 
140  case ($component instanceof F\File):
141  return $this->renderFileField($component, $default_renderer);
142 
143  case ($component instanceof F\Url):
144  return $this->renderUrlField($component, $default_renderer);
145 
146  case ($component instanceof F\Hidden):
147  return $this->renderHiddenField($component);
148 
149  case ($component instanceof F\ColorSelect):
150  return $this->renderColorSelectField($component, $default_renderer);
151 
152  case ($component instanceof F\Rating):
153  return $this->renderRatingField($component, $default_renderer);
154 
155  default:
156  $this->cannotHandleComponent($component);
157  }
158  }
159 
160  protected function wrapInFormContext(
161  FormInput $component,
162  string $label,
163  string $input_html,
164  ?string $id_for_label = null,
165  ?string $dependant_group_html = null
166  ): string {
167  $tpl = $this->getTemplate("tpl.context_form.html", true, true);
168 
169  $tpl->setVariable("LABEL", $label);
170  $tpl->setVariable("INPUT", $input_html);
171  $tpl->setVariable("UI_COMPONENT_NAME", $this->getComponentCanonicalNameAttribute($component));
172  $tpl->setVariable("INPUT_NAME", $component->getName());
173 
174  if ($component->getOnLoadCode() !== null) {
175  $binding_id = $this->bindJavaScript($component) ?? $this->createId();
176  $tpl->setVariable("BINDING_ID", $binding_id);
177  }
178 
179  if ($id_for_label) {
180  $tpl->setCurrentBlock('for');
181  $tpl->setVariable("ID", $id_for_label);
182  $tpl->parseCurrentBlock();
183  } else {
184  $tpl->touchBlock('tabindex');
185  }
186 
187  $byline = $component->getByline();
188  if ($byline) {
189  $tpl->setVariable("BYLINE", $byline);
190  }
191 
192  $required = $component->isRequired();
193  if ($required) {
194  $tpl->setCurrentBlock('required');
195  $tpl->setVariable("REQUIRED_ARIA", $this->txt('required_field'));
196  $tpl->parseCurrentBlock();
197  }
198 
199  if ($component->isDisabled()) {
200  $tpl->touchBlock("disabled");
201  }
202 
203  $error = $component->getError();
204  if ($error) {
205  $error_id = $this->createId();
206  $tpl->setVariable("ERROR_LABEL", $this->txt("ui_error"));
207  $tpl->setVariable("ERROR_ID", $error_id);
208  $tpl->setVariable("ERROR", $error);
209  if ($id_for_label) {
210  $tpl->setVariable("ERROR_FOR_ID", $id_for_label);
211  }
212  }
213 
214  if ($dependant_group_html) {
215  $tpl->setVariable("DEPENDANT_GROUP", $dependant_group_html);
216  }
217  return $tpl->get();
218  }
219 
220  protected function applyName(FormInput $component, Template $tpl): ?string
221  {
222  $name = $component->getName();
223  $tpl->setVariable("NAME", $name);
224  return $name;
225  }
226 
227  protected function bindJSandApplyId(Component\JavaScriptBindable $component, Template $tpl): string
228  {
229  $id = $this->bindJavaScript($component) ?? $this->createId();
230  $tpl->setVariable("ID", $id);
231  return $id;
232  }
233 
242  protected function applyValue(FormInput $component, Template $tpl, ?callable $escape = null): void
243  {
244  $value = $component->getValue();
245  if (!is_null($escape)) {
246  $value = $escape($value);
247  }
248  if (isset($value) && $value !== '') {
249  $tpl->setVariable("VALUE", $value);
250  }
251  }
252 
253  protected function escapeSpecialChars(): Closure
254  {
255  return function ($v) {
256  // with declare(strict_types=1) in place,
257  // htmlspecialchars will not silently convert to string anymore;
258  // therefore, the typecast must be explicit
259  return htmlspecialchars((string) $v, ENT_QUOTES);
260  };
261  }
262 
263  protected function htmlEntities(): Closure
264  {
265  return function ($v) {
266  // with declare(strict_types=1) in place,
267  // htmlentities will not silently convert to string anymore;
268  // therefore, the typecast must be explicit
269  return htmlentities((string) $v);
270  };
271  }
272 
273  protected function renderLinkField(F\Link $component, RendererInterface $default_renderer): string
274  {
275  $input_html = $default_renderer->render($component->getInputs());
276  return $this->wrapInFormContext(
277  $component,
278  $component->getLabel(),
279  $input_html,
280  );
281  }
282 
283  protected function renderTextField(F\Text $component): string
284  {
285  $tpl = $this->getTemplate("tpl.text.html", true, true);
286  $this->applyName($component, $tpl);
287 
288  if ($component->getMaxLength()) {
289  $tpl->setVariable("MAX_LENGTH", $component->getMaxLength());
290  }
291 
292  $this->applyValue($component, $tpl, $this->escapeSpecialChars());
293 
294  $label_id = $this->createId();
295  $tpl->setVariable('ID', $label_id);
296  return $this->wrapInFormContext($component, $component->getLabel(), $tpl->get(), $label_id);
297  }
298 
299  protected function renderNumericField(F\Numeric $component, RendererInterface $default_renderer): string
300  {
301  $tpl = $this->getTemplate("tpl.numeric.html", true, true);
302  $this->applyName($component, $tpl);
303  $this->applyValue($component, $tpl, $this->escapeSpecialChars());
304 
305  $label_id = $this->createId();
306  $tpl->setVariable('ID', $label_id);
307  return $this->wrapInFormContext($component, $component->getLabel(), $tpl->get(), $label_id);
308  }
309 
310  protected function renderCheckboxField(F\Checkbox $component, RendererInterface $default_renderer): string
311  {
312  $tpl = $this->getTemplate("tpl.checkbox.html", true, true);
313  $this->applyName($component, $tpl);
314 
315  if ($component->getValue()) {
316  $tpl->touchBlock("value");
317  }
318 
319  $label_id = $this->createId();
320  $tpl->setVariable('ID', $label_id);
321  return $this->wrapInFormContext($component, $component->getLabel(), $tpl->get(), $label_id);
322  }
323 
324  protected function renderOptionalGroup(F\OptionalGroup $component, RendererInterface $default_renderer): string
325  {
326  $tpl = $this->getTemplate("tpl.optionalgroup_label.html", true, true);
327  $tpl->setVariable('LABEL', $component->getLabel());
328  $tpl->setVariable("NAME", $component->getName());
329  if ($component->getValue()) {
330  $tpl->setVariable("CHECKED", 'checked="checked"');
331  }
332 
333  $label_id = $this->createId();
334  $tpl->setVariable('ID', $label_id);
335 
336  $label = $tpl->get();
337  $input_html = $default_renderer->render($component->getInputs());
338 
339  return $this->wrapInFormContext($component, $label, $input_html, $label_id);
340  }
341 
342  protected function renderSwitchableGroup(F\SwitchableGroup $component, RendererInterface $default_renderer): string
343  {
344  $value = null;
345  if ($component->getValue() !== null) {
346  list($value, ) = $component->getValue();
347  }
348 
349  $input_html = '';
350  foreach ($component->getInputs() as $key => $group) {
351  $tpl = $this->getTemplate("tpl.switchablegroup_label.html", true, true);
352  $tpl->setVariable('LABEL', $group->getLabel());
353  $tpl->setVariable("NAME", $component->getName());
354  $tpl->setVariable("VALUE", $key);
355 
356  $label_id = $this->createId();
357  $tpl->setVariable('ID', $label_id);
358 
359  if ($key == $value) {
360  $tpl->setVariable("CHECKED", 'checked="checked"');
361  }
362 
363  $input_html .= $this->wrapInFormContext(
364  $group,
365  $tpl->get(),
366  $default_renderer->render($group),
367  $label_id
368  );
369  }
370 
371 
372  return $this->wrapInFormContext(
373  $component,
374  $component->getLabel(),
375  $input_html
376  );
377  }
378 
379  protected function renderTagField(F\Tag $component, RendererInterface $default_renderer): string
380  {
381  $tpl = $this->getTemplate("tpl.tag_input.html", true, true);
382  $this->applyName($component, $tpl);
383 
384  $configuration = $component->getConfiguration();
385  $value = $component->getValue();
386 
387  if ($value) {
388  $value = array_map(
389  function ($v) {
390  return ['value' => urlencode($this->convertSpecialCharacters($v)), 'display' => $v];
391  },
392  $value
393  );
394  }
395 
396  $component = $component->withAdditionalOnLoadCode(
397  function ($id) use ($configuration, $value) {
398  $encoded = json_encode($configuration);
399  $value = json_encode($value);
400  return "il.UI.Input.tagInput.init('{$id}', {$encoded}, {$value});";
401  }
402  );
403 
404  if ($component->isDisabled()) {
405  $tpl->setVariable("DISABLED", "disabled");
406  $tpl->setVariable("READONLY", "readonly");
407  }
408 
409  $label_id = $this->createId();
410  $tpl->setVariable('ID', $label_id);
411  return $this->wrapInFormContext($component, $component->getLabel(), $tpl->get(), $label_id);
412  }
413 
414  protected function renderPasswordField(F\Password $component, RendererInterface $default_renderer): string
415  {
416  $tpl = $this->getTemplate("tpl.password.html", true, true);
417  $this->applyName($component, $tpl);
418 
419  if ($component->getRevelation()) {
420  $component = $component->withResetSignals();
421  $sig_reveal = $component->getRevealSignal();
422  $sig_mask = $component->getMaskSignal();
423  $component = $component->withAdditionalOnLoadCode(function ($id) use ($sig_reveal, $sig_mask) {
424  return
425  "$(document).on('$sig_reveal', function() {
426  const fieldContainer = document.querySelector('#$id .c-input__field .c-field-password');
427  fieldContainer.classList.add('revealed');
428  fieldContainer.getElementsByTagName('input').item(0).type='text';
429  });" .
430  "$(document).on('$sig_mask', function() {
431  const fieldContainer = document.querySelector('#$id .c-input__field .c-field-password');
432  fieldContainer.classList.remove('revealed');
433  fieldContainer.getElementsByTagName('input').item(0).type='password';
434  });";
435  });
436 
437  $f = $this->getUIFactory();
438  $glyph_reveal = $f->symbol()->glyph()->eyeopen("#")
439  ->withOnClick($sig_reveal);
440  $glyph_mask = $f->symbol()->glyph()->eyeclosed("#")
441  ->withOnClick($sig_mask);
442 
443  $tpl->setVariable('PASSWORD_REVEAL', $default_renderer->render($glyph_reveal));
444  $tpl->setVariable('PASSWORD_MASK', $default_renderer->render($glyph_mask));
445  }
446 
447  $this->applyValue($component, $tpl, $this->escapeSpecialChars());
448 
449  $label_id = $this->createId();
450  $tpl->setVariable('ID', $label_id);
451  return $this->wrapInFormContext($component, $component->getLabel(), $tpl->get(), $label_id);
452  }
453 
454  public function renderSelectField(F\Select $component, RendererInterface $default_renderer): string
455  {
456  $tpl = $this->getTemplate("tpl.select.html", true, true);
457  $this->applyName($component, $tpl);
458 
459  $value = $component->getValue();
460  //disable first option if required.
461  $tpl->setCurrentBlock("options");
462  if (!$value) {
463  $tpl->setVariable("SELECTED", 'selected="selected"');
464  }
465  if ($component->isRequired() && !$value) {
466  $tpl->setVariable("DISABLED_OPTION", "disabled");
467  $tpl->setVariable("HIDDEN", "hidden");
468  }
469 
470  if (!($value && $component->isRequired())) {
471  $tpl->setVariable("VALUE", null);
472  $tpl->setVariable("VALUE_STR", $component->isRequired() ? $this->txt('ui_select_dropdown_label') : '-');
473  $tpl->parseCurrentBlock();
474  }
475 
476  foreach ($component->getOptions() as $option_key => $option_value) {
477  $tpl->setCurrentBlock("options");
478  if ($value == $option_key) {
479  $tpl->setVariable("SELECTED", 'selected="selected"');
480  }
481  $tpl->setVariable("VALUE", $option_key);
482  $tpl->setVariable("VALUE_STR", $option_value);
483  $tpl->parseCurrentBlock();
484  }
485 
486  $label_id = $this->createId();
487  $tpl->setVariable('ID', $label_id);
488  return $this->wrapInFormContext($component, $component->getLabel(), $tpl->get(), $label_id);
489  }
490 
491  protected function renderMarkdownField(F\Markdown $component, RendererInterface $default_renderer): string
492  {
494  $component = $component->withAdditionalOnLoadCode(
495  static function ($id) use ($component): string {
496  return "
497  const id = document.querySelector('#$id .c-input__field textarea')?.id;
498  il.UI.Input.markdown.init(
499  id,
500  '{$component->getMarkdownRenderer()->getAsyncUrl()}',
501  '{$component->getMarkdownRenderer()->getParameterName()}'
502  );
503  ";
504  }
505  );
506 
507  $textarea_id = $this->createId();
508  $textarea_tpl = $this->getPreparedTextareaTemplate($component);
509  $textarea_tpl->setVariable('ID', $textarea_id);
510 
511  $markdown_tpl = $this->getTemplate("tpl.markdown.html", true, true);
512  $markdown_tpl->setVariable('TEXTAREA', $textarea_tpl->get());
513 
514  $markdown_tpl->setVariable(
515  'PREVIEW',
516  $component->getMarkdownRenderer()->render(
517  $this->htmlEntities()($component->getValue() ?? '')
518  )
519  );
520 
521  $markdown_tpl->setVariable(
522  'VIEW_CONTROLS',
523  $default_renderer->render(
524  $this->getUIFactory()->viewControl()->mode([
525  $this->txt('ui_md_input_edit') => '#',
526  $this->txt('ui_md_input_view') => '#',
527  ], "")
528  )
529  );
530 
532  $markdown_actions_glyphs = [
533  'ACTION_HEADING' => $this->getUIFactory()->symbol()->glyph()->header(),
534  'ACTION_LINK' => $this->getUIFactory()->symbol()->glyph()->link(),
535  'ACTION_BOLD' => $this->getUIFactory()->symbol()->glyph()->bold(),
536  'ACTION_ITALIC' => $this->getUIFactory()->symbol()->glyph()->italic(),
537  'ACTION_ORDERED_LIST' => $this->getUIFactory()->symbol()->glyph()->numberedlist(),
538  'ACTION_UNORDERED_LIST' => $this->getUIFactory()->symbol()->glyph()->bulletlist()
539  ];
540 
541  foreach ($markdown_actions_glyphs as $tpl_variable => $glyph) {
542  if ($component->isDisabled()) {
543  $glyph = $glyph->withUnavailableAction();
544  }
545 
546  $action = $this->getUIFactory()->button()->standard('', '#')->withSymbol($glyph);
547 
548  if ($component->isDisabled()) {
549  $action = $action->withUnavailableAction();
550  }
551 
552  $markdown_tpl->setVariable($tpl_variable, $default_renderer->render($action));
553  }
554 
555  return $this->wrapInFormContext($component, $component->getLabel(), $markdown_tpl->get());
556  }
557 
558  protected function renderTextareaField(F\Textarea $component, RendererInterface $default_renderer): string
559  {
561  $component = $component->withAdditionalOnLoadCode(
562  static function ($id): string {
563  return "
564  taId = document.querySelector('#$id .c-input__field textarea')?.id;
565  il.UI.Input.textarea.init(taId);
566  ";
567  }
568  );
569 
570  $tpl = $this->getPreparedTextareaTemplate($component);
571 
572  $label_id = $this->createId();
573  $tpl->setVariable('ID', $label_id);
574  return $this->wrapInFormContext($component, $component->getLabel(), $tpl->get(), $label_id);
575  }
576 
577  protected function getPreparedTextareaTemplate(F\Textarea $component): Template
578  {
579  $tpl = $this->getTemplate("tpl.textarea.html", true, true);
580 
581  if (0 < $component->getMaxLimit()) {
582  $tpl->setVariable('REMAINDER_TEXT', $this->txt('ui_chars_remaining'));
583  $tpl->setVariable('REMAINDER', $component->getMaxLimit() - strlen($component->getValue() ?? ''));
584  $tpl->setVariable('MAX_LIMIT', $component->getMaxLimit());
585  }
586 
587  if (null !== $component->getMinLimit()) {
588  $tpl->setVariable('MIN_LIMIT', $component->getMinLimit());
589  }
590 
591  $this->applyName($component, $tpl);
592  $this->applyValue($component, $tpl, $this->htmlEntities());
593  return $tpl;
594  }
595 
596  protected function renderRadioField(F\Radio $component, RendererInterface $default_renderer): string
597  {
598  $tpl = $this->getTemplate("tpl.radio.html", true, true);
599  $id = $this->createId();
600 
601  foreach ($component->getOptions() as $value => $label) {
602  $opt_id = $id . '_' . $value . '_opt';
603 
604  $tpl->setCurrentBlock('optionblock');
605  $tpl->setVariable("NAME", $component->getName());
606  $tpl->setVariable("OPTIONID", $opt_id);
607  $tpl->setVariable("VALUE", $value);
608  $tpl->setVariable("LABEL", $label);
609 
610  if ($component->getValue() !== null && $component->getValue() == $value) {
611  $tpl->setVariable("CHECKED", 'checked="checked"');
612  }
613  if ($component->isDisabled()) {
614  $tpl->setVariable("DISABLED", 'disabled="disabled"');
615  }
616 
617  $byline = $component->getBylineFor((string) $value);
618  if (!empty($byline)) {
619  $tpl->setVariable("BYLINE", $byline);
620  }
621 
622  $tpl->parseCurrentBlock();
623  }
624 
625  return $this->wrapInFormContext($component, $component->getLabel(), $tpl->get());
626  }
627 
628  protected function renderMultiSelectField(F\MultiSelect $component, RendererInterface $default_renderer): string
629  {
630  $tpl = $this->getTemplate("tpl.multiselect.html", true, true);
631 
632  $options = $component->getOptions();
633  if (count($options) > 0) {
634  $value = $component->getValue();
635  $name = $this->applyName($component, $tpl);
636  foreach ($options as $opt_value => $opt_label) {
637  $tpl->setCurrentBlock("option");
638  $tpl->setVariable("NAME", $name);
639  $tpl->setVariable("VALUE", $opt_value);
640  $tpl->setVariable("LABEL", $opt_label);
641 
642  if ($value && in_array($opt_value, $value)) {
643  $tpl->setVariable("CHECKED", 'checked="checked"');
644  }
645  $tpl->parseCurrentBlock();
646  }
647  } else {
648  $tpl->touchBlock("no_options");
649  }
650 
651  return $this->wrapInFormContext($component, $component->getLabel(), $tpl->get());
652  }
653 
654  protected function renderDateTimeField(F\DateTime $component, RendererInterface $default_renderer): string
655  {
656  list($component, $tpl) = $this->internalRenderDateTimeField($component, $default_renderer);
657  $label_id = $this->createId();
658  $tpl->setVariable('ID', $label_id);
659  return $this->wrapInFormContext($component, $component->getLabel(), $tpl->get(), $label_id);
660  }
661 
665  protected function internalRenderDateTimeField(F\DateTime $component, RendererInterface $default_renderer): array
666  {
667  $tpl = $this->getTemplate("tpl.datetime.html", true, true);
668  $this->applyName($component, $tpl);
669 
670  if ($component->getTimeOnly() === true) {
671  $format = $component::TIME_FORMAT;
672  $dt_type = self::TYPE_TIME;
673  } else {
674  $dt_type = self::TYPE_DATE;
675  $format = $this->getTransformedDateFormat(
676  $component->getFormat(),
677  self::DATEPICKER_FORMAT_MAPPING
678  );
679 
680  if ($component->getUseTime() === true) {
681  $format .= ' ' . $component::TIME_FORMAT;
682  $dt_type = self::TYPE_DATETIME;
683  }
684  }
685 
686  $tpl->setVariable("DTTYPE", $dt_type);
687 
688  $min_max_format = self::DATE_DATEPICKER_MINMAX_FORMAT;
689  if ($dt_type === self::TYPE_DATETIME) {
690  $min_max_format = self::DATETIME_DATEPICKER_MINMAX_FORMAT;
691  }
692 
693  $min_date = $component->getMinValue();
694  if (!is_null($min_date)) {
695  $tpl->setVariable("MIN_DATE", date_format($min_date, $min_max_format));
696  }
697  $max_date = $component->getMaxValue();
698  if (!is_null($max_date)) {
699  $tpl->setVariable("MAX_DATE", date_format($max_date, $min_max_format));
700  }
701 
702  $this->applyValue($component, $tpl, function (?string $value) use ($dt_type) {
703  if ($value !== null) {
704  $value = new \DateTimeImmutable($value);
705  return $value->format(match ($dt_type) {
706  self::TYPE_DATETIME => self::HTML5_NATIVE_DATETIME_FORMAT,
707  self::TYPE_DATE => self::HTML5_NATIVE_DATE_FORMAT,
708  self::TYPE_TIME => self::HTML5_NATIVE_TIME_FORMAT,
709  });
710  }
711  return null;
712  });
713  return [$component, $tpl];
714  }
715 
716  protected function renderDurationField(F\Duration $component, RendererInterface $default_renderer): string
717  {
718  $inputs = $component->getInputs();
719 
720  $input = array_shift($inputs); //from
721  list($input, $tpl) = $this->internalRenderDateTimeField($input, $default_renderer);
722 
723  $from_input_id = $this->createId();
724  $tpl->setVariable('ID', $from_input_id);
725  $input_html = $this->wrapInFormContext($input, $input->getLabel(), $tpl->get(), $from_input_id);
726 
727  $input = array_shift($inputs) //until
728  ->withAdditionalPickerconfig(['useCurrent' => false]);
729  list($input, $tpl) = $this->internalRenderDateTimeField($input, $default_renderer);
730  $until_input_id = $this->createId();
731  $tpl->setVariable('ID', $until_input_id);
732  $input_html .= $this->wrapInFormContext($input, $input->getLabel(), $tpl->get(), $until_input_id);
733 
734  $tpl = $this->getTemplate("tpl.duration.html", true, true);
735  $tpl->setVariable('DURATION', $input_html);
736  return $this->wrapInFormContext($component, $component->getLabel(), $tpl->get());//, $from_input_id);
737  }
738 
739  protected function renderSection(F\Section $section, RendererInterface $default_renderer): string
740  {
741  $inputs_html = $default_renderer->render($section->getInputs());
742 
743  $headline_tpl = $this->getTemplate("tpl.headlines.html", true, true);
744  $headline_tpl->setVariable("HEADLINE", $section->getLabel());
745  $nesting_level = $section->getNestingLevel() + 2;
746  if ($nesting_level > 6) {
747  $nesting_level = 6;
748  };
749  $headline_tpl->setVariable("LEVEL", $nesting_level);
750 
751  $headline_html = $headline_tpl->get();
752 
753  return $this->wrapInFormContext($section, $headline_html, $inputs_html);
754  }
755 
756  protected function renderUrlField(F\Url $component, RendererInterface $default_renderer): string
757  {
758  $tpl = $this->getTemplate("tpl.url.html", true, true);
759  $this->applyName($component, $tpl);
760  $this->applyValue($component, $tpl, $this->escapeSpecialChars());
761 
762  $label_id = $this->createId();
763  $tpl->setVariable('ID', $label_id);
764  return $this->wrapInFormContext($component, $component->getLabel(), $tpl->get(), $label_id);
765  }
766 
767  protected function renderFileField(FI\File $input, RendererInterface $default_renderer): string
768  {
769  $template = $this->getTemplate('tpl.file.html', true, true);
770  foreach ($input->getDynamicInputs() as $metadata_input) {
771  $file_info = null;
772  if (null !== ($data = $metadata_input->getValue())) {
773  $file_id = (!$input->hasMetadataInputs()) ?
774  $data : $data[$input->getUploadHandler()->getFileIdentifierParameterName()] ?? null;
775 
776  if (null !== $file_id) {
777  $file_info = $input->getUploadHandler()->getInfoResult($file_id);
778  }
779  }
780 
781  $template = $this->renderFilePreview(
782  $input,
783  $metadata_input,
784  $default_renderer,
785  $file_info,
786  $template
787  );
788  }
789 
790  $file_preview_template = $this->getTemplate('tpl.file.html', true, true);
791  $file_preview_template = $this->renderFilePreview(
792  $input,
793  $input->getTemplateForDynamicInputs(),
794  $default_renderer,
795  null,
796  $file_preview_template
797  );
798 
799  $template->setVariable('FILE_PREVIEW_TEMPLATE', $file_preview_template->get('block_file_preview'));
800 
801  $this->setHelpBlockForFileField($template, $input);
802 
803  $input = $this->initClientsideFileInput($input);
804 
805  // display the action button (to choose files).
806  $template->setVariable('ACTION_BUTTON', $default_renderer->render(
807  $this->getUIFactory()->button()->shy(
808  $this->txt('select_files_from_computer'),
809  '#'
810  )
811  ));
812 
813  return $this->wrapInFormContext(
814  $input,
815  $input->getLabel(),
816  $template->get(),
817  );
818  }
819 
820  protected function renderHiddenField(F\Hidden $input): string
821  {
822  $template = $this->getTemplate('tpl.hidden.html', true, true);
823  $this->applyName($input, $template);
824  $this->applyValue($input, $template, $this->escapeSpecialChars());
825  if ($input->isDisabled()) {
826  $template->setVariable("DISABLED", 'disabled="disabled"');
827  }
828  $this->bindJSandApplyId($input, $template);
829  return $template->get();
830  }
831 
835  public function registerResources(ResourceRegistry $registry): void
836  {
837  parent::registerResources($registry);
838  $registry->register('assets/js/tagify.min.js');
839  $registry->register('assets/css/tagify.css');
840  $registry->register('assets/js/tagInput.js');
841 
842  $registry->register('assets/js/dropzone.min.js');
843  $registry->register('assets/js/dropzone.js');
844  $registry->register('assets/js/input.js');
845  $registry->register('assets/js/core.js');
846  $registry->register('assets/js/file.js');
847  $registry->register('assets/js/input.factory.min.js');
848  }
849 
854  protected function setSignals(F\FormInput $input)
855  {
856  $signals = null;
857  foreach ($input->getTriggeredSignals() as $s) {
858  $signals[] = [
859  "signal_id" => $s->getSignal()->getId(),
860  "event" => $s->getEvent(),
861  "options" => $s->getSignal()->getOptions()
862  ];
863  }
864  if ($signals !== null) {
865  $signals = json_encode($signals);
866 
867  $input = $input->withAdditionalOnLoadCode(function ($id) use ($signals) {
868  $code = "il.UI.input.setSignalsForId('$id', $signals);";
869  return $code;
870  });
871 
872  $input = $input->withAdditionalOnLoadCode($input->getUpdateOnLoadCode());
873  }
874  return $input;
875  }
876 
883  protected function getTransformedDateFormat(
884  DateFormat\DateFormat $origin,
885  array $mapping
886  ): string {
887  $ret = '';
888  foreach ($origin->toArray() as $element) {
889  if (array_key_exists($element, $mapping)) {
890  $ret .= $mapping[$element];
891  } else {
892  $ret .= $element;
893  }
894  }
895  return $ret;
896  }
897 
898  protected function renderFilePreview(
899  FI\File $file_input,
900  FormInput $metadata_input,
901  RendererInterface $default_renderer,
902  ?FileInfoResult $file_info,
903  Template $template
904  ): Template {
905  $template->setCurrentBlock('block_file_preview');
906  $template->setVariable('REMOVAL_GLYPH', $default_renderer->render(
907  $this->getUIFactory()->symbol()->glyph()->close()->withAction("#")
908  ));
909 
910  if (null !== $file_info) {
911  $template->setVariable('FILE_NAME', $file_info->getName());
912  $template->setVariable(
913  'FILE_SIZE',
914  (string) (new DataSize($file_info->getSize(), DataSize::Byte))
915  );
916  }
917 
918  // only render expansion toggles if the input
919  // contains actual (unhidden) inputs.
920  if ($file_input->hasMetadataInputs()) {
921  $template->setVariable('EXPAND_GLYPH', $default_renderer->render(
922  $this->getUIFactory()->symbol()->glyph()->expand()->withAction("#")
923  ));
924  $template->setVariable('COLLAPSE_GLYPH', $default_renderer->render(
925  $this->getUIFactory()->symbol()->glyph()->collapse()->withAction("#")
926  ));
927  }
928 
929  $template->setVariable('METADATA_INPUTS', $default_renderer->render($metadata_input));
930 
931  $template->parseCurrentBlock();
932 
933  return $template;
934  }
935 
936  protected function initClientsideFileInput(FI\File $input): FI\File
937  {
938  return $input->withAdditionalOnLoadCode(
939  function ($id) use ($input) {
940  $current_file_count = count($input->getDynamicInputs());
941  $translations = json_encode($input->getTranslations());
942  $is_disabled = ($input->isDisabled()) ? 'true' : 'false';
943  $php_upload_limit = $this->getUploadLimitResolver()->getPhpUploadLimitInBytes();
944  $should_upload_be_chunked = ($input->getMaxFileSize() > $php_upload_limit) ? 'true' : 'false';
945  $chunk_size = (int) floor($php_upload_limit * self::FILE_UPLOAD_CHUNK_SIZE_FACTOR);
946  return "
947  $(document).ready(function () {
948  il.UI.Input.File.init(
949  '$id',
950  '{$input->getUploadHandler()->getUploadURL()}',
951  '{$input->getUploadHandler()->getFileRemovalURL()}',
952  '{$input->getUploadHandler()->getFileIdentifierParameterName()}',
953  $current_file_count,
954  {$input->getMaxFiles()},
955  {$input->getMaxFileSize()},
956  '{$this->prepareDropzoneJsMimeTypes($input->getAcceptedMimeTypes())}',
957  $is_disabled,
958  $translations,
959  $should_upload_be_chunked,
960  $chunk_size
961  );
962  });
963  ";
964  }
965  );
966  }
967 
973  protected function prepareDropzoneJsMimeTypes(array $mime_types): string
974  {
975  $mime_type_string = '';
976  foreach ($mime_types as $index => $mime_type) {
977  $mime_type_string .= (isset($mime_types[$index + 1])) ? "$mime_type," : $mime_type;
978  }
979 
980  return $mime_type_string;
981  }
982 
983  protected function renderColorSelectField(F\ColorSelect $component, RendererInterface $default_renderer): string
984  {
985  $tpl = $this->getTemplate("tpl.color_select.html", true, true);
986  $this->applyName($component, $tpl);
987  $tpl->setVariable('VALUE', $component->getValue());
988 
989  $label_id = $this->createId();
990  $tpl->setVariable('ID', $label_id);
991  return $this->wrapInFormContext($component, $component->getLabel(), $tpl->get(), $label_id);
992  }
993 
994  protected function renderRatingField(F\Rating $component, RendererInterface $default_renderer): string
995  {
996  $tpl = $this->getTemplate("tpl.rating.html", true, true);
997  $id = $this->createId();
998  $aria_description_id = $id . '_desc';
999  $tpl->setVariable('DESCRIPTION_SRC_ID', $aria_description_id);
1000 
1001  $option_count = count(FiveStarRatingScale::cases()) - 1;
1002 
1003  foreach (range($option_count, 1, -1) as $option) {
1004  $tpl->setCurrentBlock('scaleoption');
1005  $tpl->setVariable('ARIALABEL', $this->txt($option . 'stars'));
1006  $tpl->setVariable('OPT_VALUE', (string) $option);
1007  $tpl->setVariable('OPT_ID', $id . '-' . $option);
1008  $tpl->setVariable('NAME', $component->getName());
1009  $tpl->setVariable('DESCRIPTION_ID', $aria_description_id);
1010 
1011  if ($component->getValue() === FiveStarRatingScale::from((int) $option)) {
1012  $tpl->setVariable("SELECTED", ' checked="checked"');
1013  }
1014  if ($component->isDisabled()) {
1015  $tpl->setVariable("DISABLED", 'disabled="disabled"');
1016  }
1017  $tpl->parseCurrentBlock();
1018  }
1019 
1020  if (!$component->isRequired()) {
1021  $tpl->setVariable('NEUTRAL_ID', $id . '-0');
1022  $tpl->setVariable('NEUTRAL_NAME', $component->getName());
1023  $tpl->setVariable('NEUTRAL_LABEL', $this->txt('reset_stars'));
1024  $tpl->setVariable('NEUTRAL_DESCRIPTION_ID', $aria_description_id);
1025 
1026  if ($component->getValue() === FiveStarRatingScale::NONE || is_null($component->getValue())) {
1027  $tpl->setVariable('NEUTRAL_SELECTED', ' checked="checked"');
1028  }
1029  }
1030 
1031  if ($txt = $component->getAdditionalText()) {
1032  $tpl->setVariable('TEXT', $txt);
1033  }
1034 
1035  if ($component->isDisabled()) {
1036  $tpl->touchBlock('disabled');
1037  }
1038  if ($average = $component->getCurrentAverage()) {
1039  $average_title = sprintf($this->txt('rating_average'), $average);
1040  $tpl->setVariable('AVERAGE_VALUE', $average_title);
1041  $tpl->setVariable('AVERAGE_VALUE_PERCENT', $average / $option_count * self::CENTUM);
1042  }
1043 
1044  return $this->wrapInFormContext($component, $component->getLabel(), $tpl->get());
1045  }
1046 
1047  private function setHelpBlockForFileField(Template $template, FI\File $input): void
1048  {
1049  $template->setCurrentBlock('HELP_BLOCK');
1050 
1051  $template->setCurrentBlock('MAX_FILE_SIZE');
1052  $template->setVariable('FILE_SIZE_LABEL', $this->txt('file_notice'));
1053  $template->setVariable('FILE_SIZE_VALUE', new DataSize($input->getMaxFileSize(), DataSize::Byte));
1054  $template->parseCurrentBlock();
1055 
1056  $template->setCurrentBlock('MAX_FILES');
1057  $template->setVariable('FILES_LABEL', $this->txt('ui_file_upload_max_nr'));
1058  $template->setVariable('FILES_VALUE', $input->getMaxFiles());
1059  $template->parseCurrentBlock();
1060 
1061  $template->parseCurrentBlock();
1062  }
1063 }
bindJSandApplyId(Component\JavaScriptBindable $component, Template $tpl)
Definition: Renderer.php:227
Registry for resources required by rendered output like Javascript or CSS.
applyName(FormInput $component, Template $tpl)
Definition: Renderer.php:220
This implements the radio input.
Definition: Radio.php:34
renderFilePreview(FI\File $file_input, FormInput $metadata_input, RendererInterface $default_renderer, ?FileInfoResult $file_info, Template $template)
Definition: Renderer.php:898
FiveStarRatingScale
This is the scale for the Rating Input.
This implements the checkbox input.
Definition: Checkbox.php:35
setHelpBlockForFileField(Template $template, FI\File $input)
Definition: Renderer.php:1047
This implements the textarea input.
Definition: Textarea.php:33
This class provides the data size with additional information to remove the work to calculate the siz...
Definition: DataSize.php:30
trait JavaScriptBindable
Trait for components implementing JavaScriptBindable providing standard implementation.
applyValue(FormInput $component, Template $tpl, ?callable $escape=null)
Escape values for rendering with a Callable "$escape" In order to prevent XSS-attacks, values need to be stripped of special chars (such as quotes or tags).
Definition: Renderer.php:242
txt(string $id)
Get a text from the language file.
renderTagField(F\Tag $component, RendererInterface $default_renderer)
Definition: Renderer.php:379
renderLinkField(F\Link $component, RendererInterface $default_renderer)
Definition: Renderer.php:273
renderRatingField(F\Rating $component, RendererInterface $default_renderer)
Definition: Renderer.php:994
A password is used as part of credentials for authentication.
Definition: Password.php:30
registerResources(ResourceRegistry $registry)
Announce resources this renderer requires.
Definition: Renderer.php:835
getByline()
Get the byline of the input.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
Definition: Checkbox.php:21
This implements the text input.
Definition: Text.php:32
setCurrentBlock(string $name)
Set the block to work on.
renderNumericField(F\Numeric $component, RendererInterface $default_renderer)
Definition: Renderer.php:299
setVariable(string $name, $value)
Set a variable in the current block.
while($session_entry=$r->fetchRow(ilDBConstants::FETCHMODE_ASSOC)) return null
renderUrlField(F\Url $component, RendererInterface $default_renderer)
Definition: Renderer.php:756
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
Definition: Checkbox.php:21
renderRadioField(F\Radio $component, RendererInterface $default_renderer)
Definition: Renderer.php:596
A Date Format provides a format definition akin to PHP&#39;s date formatting options, but stores the sing...
Definition: DateFormat.php:26
This implements the duration input group.
Definition: Duration.php:37
renderMultiSelectField(F\MultiSelect $component, RendererInterface $default_renderer)
Definition: Renderer.php:628
getTemplate(string $name, bool $purge_unfilled_vars, bool $purge_unused_blocks)
Get template of component this renderer is made for.
This implements the multi-select input.
Definition: MultiSelect.php:31
getOnLoadCode()
Get the currently bound on load code.
renderCheckboxField(F\Checkbox $component, RendererInterface $default_renderer)
Definition: Renderer.php:310
renderDateTimeField(F\DateTime $component, RendererInterface $default_renderer)
Definition: Renderer.php:654
renderSwitchableGroup(F\SwitchableGroup $component, RendererInterface $default_renderer)
Definition: Renderer.php:342
renderColorSelectField(F\ColorSelect $component, RendererInterface $default_renderer)
Definition: Renderer.php:983
renderPasswordField(F\Password $component, RendererInterface $default_renderer)
Definition: Renderer.php:414
getValue()
Get the value that is displayed in the input client side.
renderSection(F\Section $section, RendererInterface $default_renderer)
Definition: Renderer.php:739
getTransformedDateFormat(DateFormat\DateFormat $origin, array $mapping)
Return the datetime format in a form fit for the JS-component of this input.
Definition: Renderer.php:883
renderDurationField(F\Duration $component, RendererInterface $default_renderer)
Definition: Renderer.php:716
This implements the numeric input.
Definition: Numeric.php:33
wrapInFormContext(FormInput $component, string $label, string $input_html, ?string $id_for_label=null, ?string $dependant_group_html=null)
Definition: Renderer.php:160
cannotHandleComponent(Component $component)
This method MUST be called by derived component renderers, if.
$txt
Definition: error.php:31
renderSelectField(F\Select $component, RendererInterface $default_renderer)
Definition: Renderer.php:454
This implements the URL input.
Definition: Url.php:34
register(string $name)
Add a dependency.
internalRenderDateTimeField(F\DateTime $component, RendererInterface $default_renderer)
Definition: Renderer.php:665
parseCurrentBlock()
Parse the block that is currently worked on.
$id
plugin.php for ilComponentBuildPluginInfoObjectiveTest::testAddPlugins
Definition: plugin.php:23
ilErrorHandling $error
Definition: class.ilias.php:69
This describes inputs that can be used in forms.
Definition: FormInput.php:32
withAdditionalOnLoadCode(Closure $binder)
Add some onload-code to the component instead of replacing the existing one.
renderOptionalGroup(F\OptionalGroup $component, RendererInterface $default_renderer)
Definition: Renderer.php:324
renderFileField(FI\File $input, RendererInterface $default_renderer)
Definition: Renderer.php:767
render(Component\Component $component, RendererInterface $default_renderer)
Definition: Renderer.php:84
prepareDropzoneJsMimeTypes(array $mime_types)
Appends all given mime-types to a comma-separated string.
Definition: Renderer.php:973
bindJavaScript(JavaScriptBindable $component)
Bind the component to JavaScript.