ILIAS  trunk Revision v11.0_alpha-2638-g80c1d007f79
class.FormAdapterGUI.php
Go to the documentation of this file.
1 <?php
2 
19 declare(strict_types=1);
20 
22 
26 
31 {
32  use StdObjProperties;
33 
34  protected const DEFAULT_SECTION = "@internal_default_section";
35  protected string $submit_caption = "";
36  protected \ilLanguage $lng;
37  protected const ASYNC_NONE = 0;
38  protected const ASYNC_MODAL = 1;
39  protected const ASYNC_ON = 2;
40  protected \ILIAS\Data\Factory $data;
41  protected \ilObjUser $user;
42  protected string $last_key = "";
43  protected \ILIAS\Refinery\Factory $refinery;
44 
45  protected string $title = "";
46  protected array $values = [];
47  protected array $disable = [];
48 
52  protected $raw_data = null;
53  protected \ILIAS\HTTP\Services $http;
54  protected \ilCtrlInterface $ctrl;
55  protected \ILIAS\DI\UIServices $ui;
56  protected array $fields = [];
57  protected array $field_path = [];
58  protected array $sections = [self::DEFAULT_SECTION => ["title" => "", "description" => "", "fields" => []]];
59  protected string $current_section = self::DEFAULT_SECTION;
60  protected array $section_of_field = [];
61  protected $class_path;
62  protected string $cmd = self::DEFAULT_SECTION;
63  protected ?Form\Standard $form = null;
64  protected array $upload_handler = [];
65  protected int $async_mode = self::ASYNC_NONE;
66  protected \ilGlobalTemplateInterface $main_tpl;
67  protected ?array $current_switch = null;
68  protected ?array $current_optional = null;
69  protected ?array $current_group = null;
70  protected static bool $initialised = false;
71 
75  public function __construct(
77  string $cmd,
78  string $submit_caption = ""
79  ) {
80  global $DIC;
81  $this->class_path = $class_path;
82  $this->cmd = $cmd;
83  $this->ui = $DIC->ui();
84  $this->ctrl = $DIC->ctrl();
85  $this->http = $DIC->http();
86  $this->lng = $DIC->language();
87  $this->refinery = $DIC->refinery();
88  $this->main_tpl = $DIC->ui()->mainTemplate();
89  $this->user = $DIC->user();
90  $this->data = new \ILIAS\Data\Factory();
91  $this->submit_caption = $submit_caption;
92  self::initJavascript();
93  $this->initStdObjProperties($DIC);
94  $this->lng->loadLanguageModule("rep");
95  }
96 
97  public static function getOnLoadCode(): string
98  {
99  return "il.repository.ui.init();\n" .
100  "il.repository.core.init('" . ILIAS_HTTP_PATH . "')";
101  }
102 
103  public static function initJavascript(): void
104  {
105  global $DIC;
106 
107  if (!isset($DIC["ui.factory"])) {
108  return;
109  }
110 
111  $f = $DIC->ui()->factory();
112  $r = $DIC->ui()->renderer();
113  if (!self::$initialised) {
114  $main_tpl = $DIC->ui()->mainTemplate();
115  $debug = false;
116  if ($debug) {
117  $main_tpl->addJavaScript("../components/ILIAS/Repository/resources/repository.js");
118  } else {
119  $main_tpl->addJavaScript("assets/js/repository.js");
120  }
121 
122  $main_tpl->addOnLoadCode(self::getOnLoadCode());
123 
124  // render dummy components to load the necessary .js needed for async processing
125  $d = [];
126  $d[] = $f->input()->field()->text("");
127  $r->render($d);
128  self::$initialised = true;
129  }
130  }
131 
132  public function asyncModal(): self
133  {
134  $this->async_mode = self::ASYNC_MODAL;
135  return $this;
136  }
137 
138  public function async(): self
139  {
140  $this->async_mode = self::ASYNC_ON;
141  return $this;
142  }
143 
144  public function syncModal(): self
145  {
146  return $this;
147  }
148 
149  public function isSentAsync(): bool
150  {
151  return ($this->async_mode !== self::ASYNC_NONE);
152  }
153 
154  public function getTitle(): string
155  {
156  return $this->title;
157  }
158 
159  public function section(
160  string $key,
161  string $title,
162  string $description = ""
163  ): self {
164  if ($this->title == "") {
165  $this->title = $title;
166  }
167 
168  $this->sections[$key] = [
169  "title" => $title,
170  "description" => $description,
171  "fields" => []
172  ];
173  $this->current_section = $key;
174  return $this;
175  }
176 
177  public function text(
178  string $key,
179  string $title,
180  string $description = "",
181  ?string $value = null
182  ): self {
183  $this->values[$key] = $value;
184  $field = $this->ui->factory()->input()->field()->text($title, $description);
185  if (!is_null($value)) {
186  $field = $field->withValue($value);
187  }
188  $this->addField($key, $field);
189  return $this;
190  }
191 
192  public function checkbox(
193  string $key,
194  string $title,
195  string $description = "",
196  ?bool $value = null
197  ): self {
198  $this->values[$key] = $value;
199  $field = $this->ui->factory()->input()->field()->checkbox($title, $description);
200  if (!is_null($value)) {
201  $field = $field->withValue($value);
202  }
203  $this->addField($key, $field);
204  return $this;
205  }
206 
207  public function hidden(
208  string $key,
209  string $value
210  ): self {
211  $this->values[$key] = $value;
212  $field = $this->ui->factory()->input()->field()->hidden();
213  $field = $field->withValue($value);
214  $this->addField($key, $field);
215  return $this;
216  }
217 
218  public function required($required = true): self
219  {
220  if ($required && ($field = $this->getLastField())) {
221  if ($field instanceof \ILIAS\UI\Component\Input\Field\Text) {
222  $field = $field->withRequired(true, new NotEmpty(
223  new Factory(),
224  $this->lng
225  ));
226  } else {
227  $field = $field->withRequired(true);
228  }
229  $this->replaceLastField($field);
230  }
231  return $this;
232  }
233 
234  public function disabled($disabled = true): self
235  {
236  if ($disabled && ($field = $this->getLastField())) {
237  $field = $field->withDisabled(true);
238  $this->disable[$this->last_key] = true;
239  $this->replaceLastField($field);
240  }
241  return $this;
242  }
243 
244  public function textarea(
245  string $key,
246  string $title,
247  string $description = "",
248  ?string $value = null
249  ): self {
250  $this->values[$key] = $value;
251  $field = $this->ui->factory()->input()->field()->textarea($title, $description);
252  if (!is_null($value)) {
253  $field = $field->withValue($value);
254  }
255  $this->addField($key, $field);
256  return $this;
257  }
258 
259  public function number(
260  string $key,
261  string $title,
262  string $description = "",
263  ?int $value = null,
264  ?int $min_value = null,
265  ?int $max_value = null
266  ): self {
267  $this->values[$key] = $value;
268  $trans = [];
269  if (!is_null($min_value)) {
270  $trans[] = $this->refinery->int()->isGreaterThanOrEqual($min_value);
271  }
272  if (!is_null($max_value)) {
273  $trans[] = $this->refinery->int()->isLessThanOrEqual($max_value);
274  }
275  $field = $this->ui->factory()->input()->field()->numeric($title, $description);
276  if (count($trans) > 0) {
277  $field = $field->withAdditionalTransformation($this->refinery->logical()->parallel($trans));
278  }
279  if (!is_null($value)) {
280  $field = $field->withValue($value);
281  }
282  $this->addField($key, $field);
283  return $this;
284  }
285 
286  public function date(
287  string $key,
288  string $title,
289  string $description = "",
290  ?\ilDate $value = null
291  ): self {
292  $this->values[$key] = $value;
293  $field = $this->ui->factory()->input()->field()->dateTime($title, $description);
294 
295  $format = $this->user->getDateFormat();
296  $dt_format = (string) $format;
297 
298  $field = $field->withFormat($format);
299  if (!is_null($value)) {
300  $field = $field->withValue(
301  (new \DateTime($value->get(IL_CAL_DATE)))->format($dt_format)
302  );
303  }
304  $this->addField($key, $field);
305  return $this;
306  }
307 
308  public function dateTime(
309  string $key,
310  string $title,
311  string $description = "",
312  ?\ilDateTime $value = null
313  ): self {
314  $this->values[$key] = $value;
315  $field = $this->ui->factory()->input()->field()->dateTime($title, $description)->withUseTime(true);
316 
317  if ((int) $this->user->getTimeFormat() === \ilCalendarSettings::TIME_FORMAT_12) {
318  $dt_format = $this->data->dateFormat()->withTime12($this->user->getDateFormat());
319  } else {
320  $dt_format = $this->data->dateFormat()->withTime24($this->user->getDateFormat());
321  }
322  $field = $field->withFormat($dt_format);
323  if (!is_null($value) && !is_null($value->get(IL_CAL_DATETIME))) {
324  $field = $field->withValue(
325  (new \DateTime($value->get(IL_CAL_DATETIME)))->format(
326  ((string) $dt_format)
327  )
328  );
329  }
330  $this->addField($key, $field);
331  return $this;
332  }
333 
334  public function dateTimeDuration(
335  string $key,
336  string $title,
337  string $description = "",
338  ?\ilDateTime $from = null,
339  ?\ilDateTime $to = null,
340  string $label_from = "",
341  string $label_to = ""
342  ): self {
343  $this->values[$key] = [$from, $to];
344  if ($label_from === "") {
345  $label_from = $this->lng->txt("rep_activation_limited_start");
346  }
347  if ($label_to === "") {
348  $label_to = $this->lng->txt("rep_activation_limited_end");
349  }
350  $field = $this->ui->factory()->input()->field()->duration($title, $description)->withUseTime(true)->withLabels($label_from, $label_to);
351 
352  if ((int) $this->user->getTimeFormat() === \ilCalendarSettings::TIME_FORMAT_12) {
353  $dt_format = $this->data->dateFormat()->withTime12($this->user->getDateFormat());
354  } else {
355  $dt_format = $this->data->dateFormat()->withTime24($this->user->getDateFormat());
356  }
357  $field = $field->withFormat($dt_format);
358  $val_from = $val_to = null;
359  if (!is_null($from) && !is_null($from->get(IL_CAL_DATETIME))) {
360  $val_from = (new \DateTime(
361  $from->get(IL_CAL_DATETIME)
362  ))->format((string) $dt_format);
363  }
364  if (!is_null($to) && !is_null($to->get(IL_CAL_DATETIME))) {
365  $val_to = (new \DateTime(
366  $to->get(IL_CAL_DATETIME)
367  ))->format((string) $dt_format);
368  }
369  $field = $field->withValue([$val_from, $val_to]);
370  $this->addField($key, $field);
371  return $this;
372  }
373 
377  protected function getDateTimeData(?\DateTimeImmutable $value, $use_time = false)
378  {
379  if (is_null($value)) {
380  return null;
381  }
382  if ($use_time) {
383  return new \ilDateTime($value->format("Y-m-d H:i:s"), IL_CAL_DATETIME);
384  }
385  return new \ilDate($value->format("Y-m-d"), IL_CAL_DATE);
386  }
387 
388  public function select(
389  string $key,
390  string $title,
391  array $options,
392  string $description = "",
393  ?string $value = null
394  ): self {
395  $this->values[$key] = $value;
396  $field = $this->ui->factory()->input()->field()->select($title, $options, $description);
397  if (!is_null($value)) {
398  $field = $field->withValue($value);
399  }
400  $this->addField(
401  $key,
402  $field
403  );
404  return $this;
405  }
406 
407  public function radio(
408  string $key,
409  string $title,
410  string $description = "",
411  ?string $value = null
412  ): self {
413  $this->values[$key] = $value;
414  $field = $this->ui->factory()->input()->field()->radio($title, $description);
415  if (!is_null($value)) {
416  $field = $field->withOption($value, ""); // dummy to prevent exception, will be overwritten by radioOption
417  $field = $field->withValue($value);
418  }
419  $this->addField(
420  $key,
421  $field
422  );
423  return $this;
424  }
425 
426  public function radioOption(string $value, string $title, string $description = ""): self
427  {
428  if ($field = $this->getLastField()) {
429  $field = $field->withOption($value, $title, $description);
430  $this->replaceLastField($field);
431  }
432  return $this;
433  }
434 
435  public function switch(
436  string $key,
437  string $title,
438  string $description = "",
439  ?string $value = null
440  ): self {
441  $this->values[$key] = $value;
442  $this->current_switch = [
443  "key" => $key,
444  "title" => $title,
445  "description" => $description,
446  "value" => $value,
447  "groups" => []
448  ];
449  return $this;
450  }
451 
452  public function optional(
453  string $key,
454  string $title,
455  string $description = "",
456  ?bool $value = null
457  ): self {
458  $this->values[$key] = $value;
459  $this->current_optional = [
460  "key" => $key,
461  "title" => $title,
462  "description" => $description,
463  "value" => $value,
464  "group" => []
465  ];
466  return $this;
467  }
468 
469  public function group(string $key, string $title, string $description = "", $disabled = false): self
470  {
471  $this->endCurrentGroup();
472  $this->current_group = [
473  "key" => $key,
474  "title" => $title,
475  "description" => $description,
476  "disabled" => $disabled,
477  "fields" => []
478  ];
479  return $this;
480  }
481 
482  protected function endCurrentGroup(): void
483  {
484  if (!is_null($this->current_group)) {
485  if (!is_null($this->current_switch)) {
486  $fields = [];
487  foreach ($this->current_group["fields"] as $key) {
488  $fields[$key] = $this->fields[$key];
489  }
490  $this->current_switch["groups"][$this->current_group["key"]] =
491  $this->ui->factory()->input()->field()->group(
492  $fields,
493  $this->current_group["title"]
494  )->withByline($this->current_group["description"]);
495  if ($this->current_group["disabled"]) {
496  $this->current_switch["groups"][$this->current_group["key"]] =
497  $this->current_switch["groups"][$this->current_group["key"]]
498  ->withDisabled(true);
499  }
500  }
501  }
502  $this->current_group = null;
503  }
504 
505  public function end(): self
506  {
507  $this->endCurrentGroup();
508  if (!is_null($this->current_switch)) {
509  $field = $this->ui->factory()->input()->field()->switchableGroup(
510  $this->current_switch["groups"],
511  $this->current_switch["title"],
512  $this->current_switch["description"]
513  );
514  if (!is_null($this->current_switch["value"])) {
515  $cvalue = $this->current_switch["value"];
516  if (isset($this->current_switch["groups"][$cvalue])) {
517  $field = $field->withValue($cvalue);
518  }
519  }
520  $key = $this->current_switch["key"];
521  $this->current_switch = null;
522  $this->addField($key, $field);
523  }
524  if (!is_null($this->current_optional)) {
525  $field = $this->ui->factory()->input()->field()->optionalGroup(
526  $this->current_optional["fields"],
527  $this->current_optional["title"],
528  $this->current_optional["description"]
529  );
530  if ($this->current_optional["value"]) {
531  $value = [];
532  foreach ($this->current_optional["fields"] as $key => $input) {
533  $value[$key] = $input->getValue();
534  }
535  $field = $field->withValue($value);
536  } else {
537  $field = $field->withValue(null);
538  }
539  $key = $this->current_optional["key"];
540  $this->current_optional = null;
541  $this->addField($key, $field);
542  }
543  return $this;
544  }
545 
546  public function file(
547  string $key,
548  string $title,
549  \Closure $result_handler,
550  string $id_parameter,
551  string $description = "",
552  ?int $max_files = null,
553  array $mime_types = [],
554  array $ctrl_path = [],
555  string $logger_id = ""
556  ): self {
557  $this->upload_handler[$key] = new \ilRepoStandardUploadHandlerGUI(
558  $result_handler,
559  $id_parameter,
560  $logger_id,
561  $ctrl_path
562  );
563 
564  foreach (["application/x-compressed", "application/x-zip-compressed"] as $zipmime) {
565  if (in_array("application/zip", $mime_types) &&
566  !in_array($zipmime, $mime_types)) {
567  $mime_types[] = $zipmime;
568  }
569  }
570 
571  if (count($mime_types) > 0) {
572  $description .= $this->lng->txt("rep_allowed_types") . ": " .
573  implode(", ", $mime_types);
574  }
575 
576  $field = $this->ui->factory()->input()->field()->file(
577  $this->upload_handler[$key],
578  $title,
579  $description
580  );
581  // not necessary, see https://github.com/ILIAS-eLearning/ILIAS/pull/9314
582  //->withMaxFileSize((int) \ilFileUtils::getPhpUploadSizeLimitInBytes());
583  if (!is_null($max_files)) {
584  $field = $field->withMaxFiles($max_files);
585  }
586  if (count($mime_types) > 0) {
587  $field = $field->withAcceptedMimeTypes($mime_types);
588  }
589 
590  $this->addField(
591  $key,
592  $field
593  );
594  return $this;
595  }
596 
598  {
599  if (!isset($this->upload_handler[$key])) {
600  throw new \ilException("Unknown file upload field: " . $key);
601  }
602  return $this->upload_handler[$key];
603  }
604 
605 
606  protected function addField(string $key, FormInput $field, $supress_0_key = false): void
607  {
608  if ($key === "") {
609  throw new \ilException("Missing Input Key: " . $key);
610  }
611  if (isset($this->field[$key])) {
612  throw new \ilException("Duplicate Input Key: " . $key);
613  }
614  $field_path = [];
615  if ($this->current_section !== self::DEFAULT_SECTION) {
616  $field_path[] = $this->current_section;
617  }
618  if (!is_null($this->current_group)) {
619  $this->current_group["fields"][] = $key;
620  if (!is_null($this->current_switch)) {
621  $field_path[] = $this->current_switch["key"];
622  $field_path[] = 1; // the value of subitems in SwitchableGroup are in the 1 key of the raw data
623  $field_path[] = $key;
624  }
625  } elseif (!is_null($this->current_optional)) {
626  $field_path[] = $this->current_optional["key"];
627  $this->current_optional["fields"][$key] = $field;
628  $field_path[] = $key;
629  } else {
630  $this->sections[$this->current_section]["fields"][] = $key;
631  $field_path[] = $key;
632  if ($field instanceof \ILIAS\UI\Component\Input\Field\SwitchableGroup) {
633  $field_path[] = 0; // the value of the SwitchableGroup is in the 0 key of the raw data
634  }
635  if ($field instanceof \ILIAS\UI\Component\Input\Field\OptionalGroup) {
636  //$field_path[] = 0; // the value of the SwitchableGroup is in the 0 key of the raw data
637  }
638  if ($field instanceof \ILIAS\UI\Component\Input\Field\File) {
639  if (!$supress_0_key) { // needed for tiles, that come with a custom transformation omitting the 0
640  $field_path[] = 0; // the value of File Inputs is in the 0 key of the raw data
641  }
642  }
643  }
644  $this->fields[$key] = $field;
645  $this->field_path[$key] = $field_path;
646  $this->last_key = $key;
647  $this->form = null;
648  }
649 
650  protected function getFieldForKey(string $key): FormInput
651  {
652  if (!isset($this->fields[$key])) {
653  throw new \ilException("Unknown Key: " . $key);
654  }
655  return $this->fields[$key];
656  }
657 
658  protected function getLastField(): ?FormInput
659  {
660  return $this->fields[$this->last_key] ?? null;
661  }
662 
663  protected function replaceLastField(FormInput $field): void
664  {
665  if ($this->last_key !== "") {
666  $this->fields[$this->last_key] = $field;
667  }
668  }
669 
670  public function getForm(): Form\Standard
671  {
672  $ctrl = $this->ctrl;
673 
674  if (is_null($this->form)) {
675  $async = ($this->async_mode !== self::ASYNC_NONE);
676  $action = "";
677  if (!is_null($this->class_path)) {
678  $action = $ctrl->getLinkTargetByClass($this->class_path, $this->cmd, "", $async);
679  }
680  $inputs = [];
681  foreach ($this->sections as $sec_key => $section) {
682  if ($sec_key === self::DEFAULT_SECTION) {
683  foreach ($this->sections[$sec_key]["fields"] as $f_key) {
684  $inputs[$f_key] = $this->getFieldForKey($f_key);
685  }
686  } elseif (count($this->sections[$sec_key]["fields"]) > 0) {
687  $sec_inputs = [];
688  foreach ($this->sections[$sec_key]["fields"] as $f_key) {
689  $sec_inputs[$f_key] = $this->getFieldForKey($f_key);
690  }
691  $inputs[$sec_key] = $this->ui->factory()->input()->field()->section(
692  $sec_inputs,
693  $section["title"],
694  $section["description"]
695  );
696  }
697  }
698  $this->form = $this->ui->factory()->input()->container()->form()->standard(
699  $action,
700  $inputs
701  );
702  if ($this->submit_caption !== "") {
703  $this->form = $this->form->withSubmitLabel($this->submit_caption);
704  }
705  }
706  return $this->form;
707  }
708 
709  public function getSubmitLabel(): string
710  {
711  return $this->getForm()->getSubmitLabel() ?? $this->lng->txt("save");
712  }
713 
714  protected function _getData(): void
715  {
716  if (is_null($this->raw_data)) {
717  $request = $this->http->request();
718  $this->form = $this->getForm()->withRequest($request);
719  $this->raw_data = $this->form->getData();
720  }
721  }
722 
723  public function isValid(): bool
724  {
725  $this->_getData();
726  return !(is_null($this->raw_data));
727  }
728 
732  public function getData(string $key)
733  {
734  $this->_getData();
735 
736  if (!isset($this->fields[$key])) {
737  return null;
738  }
739 
740  if (isset($this->disable[$key])) {
741  return $this->values[$key];
742  }
743 
744  $value = $this->raw_data;
745  foreach ($this->field_path[$key] as $path_key) {
746  if (!isset($value[$path_key])) {
747  return null;
748  }
749  $value = $value[$path_key];
750  }
751 
752  $field = $this->getFieldForKey($key);
753 
754  if ($field instanceof \ILIAS\UI\Component\Input\Field\DateTime) {
756  $value = $this->getDateTimeData($value, $field->getUseTime());
757  }
758 
759  if ($field instanceof \ILIAS\UI\Component\Input\Field\Duration) {
761  $value = [
762  $this->getDateTimeData($value["start"], $field->getUseTime()),
763  $this->getDateTimeData($value["end"], $field->getUseTime()),
764  ];
765  }
766 
767  if ($field instanceof \ILIAS\UI\Component\Input\Field\OptionalGroup) {
768  $value = is_array($value);
769  }
770 
771  return $value;
772  }
773 
774  public function render(): string
775  {
776  if ($this->async_mode === self::ASYNC_NONE && !$this->ctrl->isAsynch()) {
777  $html = $this->ui->renderer()->render($this->getForm());
778  } else {
779  $html = $this->ui->renderer()->renderAsync($this->getForm()) . "<script>" . $this->getOnLoadCode() . "</script>";
780  }
781  return $html;
782  }
783 }
optional(string $key, string $title, string $description="", ?bool $value=null)
const IL_CAL_DATETIME
radio(string $key, string $title, string $description="", ?string $value=null)
text(string $key, string $title, string $description="", ?string $value=null)
checkbox(string $key, string $title, string $description="", ?bool $value=null)
getLinkTargetByClass( $a_class, ?string $a_cmd=null, ?string $a_anchor=null, bool $is_async=false, bool $has_xml_style=false)
Returns a link target for the given information.
Interface Observer Contains several chained tasks and infos about them.
select(string $key, string $title, array $options, string $description="", ?string $value=null)
dateTimeDuration(string $key, string $title, string $description="", ?\ilDateTime $from=null, ?\ilDateTime $to=null, string $label_from="", string $label_to="")
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
Definition: Factory.php:21
__construct( $class_path, string $cmd, string $submit_caption="")
Builds a Color from either hex- or rgb values.
Definition: Factory.php:30
This implements commonalities between all forms.
Definition: Form.php:33
section(string $key, string $title, string $description="")
while($session_entry=$r->fetchRow(ilDBConstants::FETCHMODE_ASSOC)) return null
file(string $key, string $title, \Closure $result_handler, string $id_parameter, string $description="", ?int $max_files=null, array $mime_types=[], array $ctrl_path=[], string $logger_id="")
initStdObjProperties(Container $DIC)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
number(string $key, string $title, string $description="", ?int $value=null, ?int $min_value=null, ?int $max_value=null)
global $DIC
Definition: shib_login.php:26
addField(string $key, FormInput $field, $supress_0_key=false)
dateTime(string $key, string $title, string $description="", ?\ilDateTime $value=null)
form( $class_path, string $cmd, string $submit_caption="")
const IL_CAL_DATE
radioOption(string $value, string $title, string $description="")
This describes commonalities between all inputs.
Definition: Input.php:46
date(string $key, string $title, string $description="", ?\ilDate $value=null)
group(string $key, string $title, string $description="", $disabled=false)
textarea(string $key, string $title, string $description="", ?string $value=null)
$r