1<?php 2 3/* Copyright (c) 2016 Richard Klees <richard.klees@concepts-and-training.de> Extended GPL, see docs/LICENSE */ 4 5namespace ILIAS\UI\Implementation\Component\Input\Field; 6 7use ILIAS\Data\DateFormat as DateFormat; 8use ILIAS\UI\Component; 9use ILIAS\UI\Component\Input\Field\MultiSelect; 10use ILIAS\UI\Component\Input\Field\Password; 11use ILIAS\UI\Component\Input\Field\Select; 12use ILIAS\UI\Implementation\Render\AbstractComponentRenderer; 13use ILIAS\UI\Implementation\Render\ResourceRegistry; 14use ILIAS\UI\Implementation\Render\Template; 15use ILIAS\UI\Renderer as RendererInterface; 16 17/** 18 * Class Renderer 19 * 20 * @package ILIAS\UI\Implementation\Component\Input 21 */ 22class Renderer extends AbstractComponentRenderer 23{ 24 const DATEPICKER_MINMAX_FORMAT = 'Y/m/d'; 25 26 const DATEPICKER_FORMAT_MAPPING = [ 27 'd' => 'DD', 28 'jS' => 'Do', 29 'l' => 'dddd', 30 'D' => 'dd', 31 'S' => 'o', 32 'W' => '', 33 'm' => 'MM', 34 'F' => 'MMMM', 35 'M' => 'MMM', 36 'Y' => 'YYYY', 37 'y' => 'YY' 38 ]; 39 40 /** 41 * @inheritdoc 42 */ 43 public function render(Component\Component $component, RendererInterface $default_renderer) 44 { 45 /** 46 * @var $component Input 47 */ 48 $this->checkComponent($component); 49 50 $input_tpl = null; 51 $id = null; 52 $dependant_group_html = null; 53 54 if ($component instanceof Component\Input\Field\Text) { 55 $input_tpl = $this->getTemplate("tpl.text.html", true, true); 56 } elseif ($component instanceof Component\Input\Field\Numeric) { 57 $input_tpl = $this->getTemplate("tpl.numeric.html", true, true); 58 } elseif ($component instanceof Component\Input\Field\Checkbox) { 59 $input_tpl = $this->getTemplate("tpl.checkbox.html", true, true); 60 } elseif ($component instanceof Component\Input\Field\OptionalGroup) { 61 $input_tpl = $this->getTemplate("tpl.checkbox.html", true, true); 62 $component = $component->withAdditionalOnLoadCode(function ($id) { 63 return $this->getOptionalGroupOnLoadCode($id); 64 }); 65 $dependant_group_html = $this->renderFieldGroups($component, $default_renderer); 66 $id = $this->bindJavaScript($component); 67 return $this->renderInputFieldWithContext($default_renderer, $input_tpl, $component, $id, $dependant_group_html); 68 } elseif ($component instanceof Component\Input\Field\SwitchableGroup) { 69 return $this->renderSwitchableGroupField($component, $default_renderer); 70 } elseif ($component instanceof Component\Input\Field\Tag) { 71 $input_tpl = $this->getTemplate("tpl.tag_input.html", true, true); 72 } elseif ($component instanceof Password) { 73 $input_tpl = $this->getTemplate("tpl.password.html", true, true); 74 } elseif ($component instanceof Component\Input\Field\File) { 75 $input_tpl = $this->getTemplate("tpl.file.html", true, true); 76 $input_tpl->setVariable('BUTTON', $default_renderer->render($this->getUIFactory()->button()->shy($this->txt('select_files_from_computer'), "#"))); 77 } elseif ($component instanceof Select) { 78 $input_tpl = $this->getTemplate("tpl.select.html", true, true); 79 } elseif ($component instanceof Component\Input\Field\Textarea) { 80 $input_tpl = $this->getTemplate("tpl.textarea.html", true, true); 81 } elseif ($component instanceof Component\Input\Field\Radio) { 82 return $this->renderRadioField($component, $default_renderer); 83 } elseif ($component instanceof MultiSelect) { 84 $input_tpl = $this->getTemplate("tpl.multiselect.html", true, true); 85 } elseif ($component instanceof Component\Input\Field\DateTime) { 86 $input_tpl = $this->getTemplate("tpl.datetime.html", true, true); 87 } elseif ($component instanceof Component\Input\Field\Group) { 88 return $this->renderFieldGroups($component, $default_renderer); 89 } else { 90 throw new \LogicException("Cannot render '" . get_class($component) . "'"); 91 } 92 93 return $this->renderInputFieldWithContext($default_renderer, $input_tpl, $component, $id); 94 } 95 96 97 /** 98 * @inheritdoc 99 */ 100 public function registerResources(ResourceRegistry $registry) 101 { 102 parent::registerResources($registry); 103 $registry->register('./libs/bower/bower_components/typeahead.js/dist/typeahead.bundle.js'); 104 $registry->register('./libs/bower/bower_components/bootstrap-tagsinput/dist/bootstrap-tagsinput.min.js'); 105 $registry->register('./libs/bower/bower_components/bootstrap-tagsinput/dist/bootstrap-tagsinput-typeahead.css'); 106 $registry->register('./src/UI/templates/js/Input/Field/tagInput.js'); 107 $registry->register('./src/UI/templates/js/Input/Field/textarea.js'); 108 $registry->register('./src/UI/templates/js/Input/Field/input.js'); 109 $registry->register('./src/UI/templates/js/Input/Field/duration.js'); 110 $registry->register('./libs/bower/bower_components/dropzone/dist/min/dropzone.min.js'); 111 $registry->register('./src/UI/templates/js/Input/Field/file.js'); 112 } 113 114 115 /** 116 * @param Input $input 117 * @return Input|\ILIAS\UI\Implementation\Component\JavaScriptBindable 118 */ 119 protected function setSignals(Input $input) 120 { 121 $signals = null; 122 foreach ($input->getTriggeredSignals() as $s) { 123 $signals[] = [ 124 "signal_id" => $s->getSignal()->getId(), 125 "event" => $s->getEvent(), 126 "options" => $s->getSignal()->getOptions() 127 ]; 128 } 129 if ($signals !== null) { 130 $signals = json_encode($signals); 131 132 133 $input = $input->withAdditionalOnLoadCode(function ($id) use ($signals) { 134 $code = "il.UI.input.setSignalsForId('$id', $signals);"; 135 return $code; 136 }); 137 138 $input = $input->withAdditionalOnLoadCode($input->getUpdateOnLoadCode()); 139 } 140 return $input; 141 } 142 143 144 /** 145 * @param Group $group 146 * @param RendererInterface $default_renderer 147 * 148 * @return string 149 */ 150 protected function renderFieldGroups(Group $group, RendererInterface $default_renderer) 151 { 152 if ($group instanceof Component\Input\Field\Section) { 153 /** 154 * @var $group Section 155 */ 156 return $this->renderSection($group, $default_renderer); 157 } elseif ($group instanceof Component\Input\Field\Duration) { 158 /** 159 * @var $group Duration 160 */ 161 return $this->renderDurationInput($group, $default_renderer); 162 } 163 164 $inputs = ""; 165 foreach ($group->getInputs() as $input) { 166 $inputs .= $default_renderer->render($input); 167 } 168 169 return $inputs; 170 } 171 172 /** 173 * @param Component\JavascriptBindable $component 174 * @param $tpl 175 */ 176 protected function maybeRenderId(Component\JavascriptBindable $component, Template $tpl) 177 { 178 $id = $this->bindJavaScript($component); 179 if ($id !== null) { 180 $tpl->setCurrentBlock("id"); 181 $tpl->setVariable("ID", $id); 182 $tpl->parseCurrentBlock(); 183 } 184 } 185 186 187 /** 188 * @param Section $section 189 * @param RendererInterface $default_renderer 190 * 191 * @return string 192 */ 193 protected function renderSection(Section $section, RendererInterface $default_renderer) 194 { 195 $section_tpl = $this->getTemplate("tpl.section.html", true, true); 196 $section_tpl->setVariable("LABEL", $section->getLabel()); 197 198 if ($section->getByline() !== null) { 199 $section_tpl->setCurrentBlock("byline"); 200 $section_tpl->setVariable("BYLINE", $section->getByline()); 201 $section_tpl->parseCurrentBlock(); 202 } 203 204 if ($section->getError() !== null) { 205 $section_tpl->setCurrentBlock("error"); 206 $section_tpl->setVariable("ERROR", $section->getError()); 207 $section_tpl->parseCurrentBlock(); 208 } 209 $inputs_html = ""; 210 211 foreach ($section->getInputs() as $input) { 212 $inputs_html .= $default_renderer->render($input); 213 } 214 $section_tpl->setVariable("INPUTS", $inputs_html); 215 216 return $section_tpl->get(); 217 } 218 219 /** 220 * @param RendererInterface $default_renderer 221 * @param Template $input_tpl 222 * @param Input $input 223 * @param null $id 224 * @param null $dependant_group_html 225 * 226 * @return string 227 */ 228 protected function renderInputFieldWithContext(RendererInterface $default_renderer, Template $input_tpl, Input $input, $id = null, $dependant_group_html = null) 229 { 230 $tpl = $this->getTemplate("tpl.context_form.html", true, true); 231 /** 232 * TODO: should we throw an error in case for no name or render without name? 233 * 234 * if(!$input->getName()){ 235 * throw new \LogicException("Cannot render '".get_class($input)."' no input name given. 236 * Is there a name source attached (is this input packed into a container attaching 237 * a name source)?"); 238 * } */ 239 if ($input->getName()) { 240 $tpl->setVariable("NAME", $input->getName()); 241 } else { 242 $tpl->setVariable("NAME", ""); 243 } 244 245 $tpl->setVariable("LABEL", $input->getLabel()); 246 $tpl->setVariable("INPUT", $this->renderInputField($input_tpl, $input, $id, $default_renderer)); 247 248 if ($input->getByline() !== null) { 249 $tpl->setCurrentBlock("byline"); 250 $tpl->setVariable("BYLINE", $input->getByline()); 251 $tpl->parseCurrentBlock(); 252 } 253 254 if ($input->isRequired()) { 255 $tpl->touchBlock("required"); 256 } 257 258 if ($input->getError() !== null) { 259 $tpl->setCurrentBlock("error"); 260 $tpl->setVariable("ERROR", $input->getError()); 261 $tpl->parseCurrentBlock(); 262 } 263 264 if ($dependant_group_html !== null) { 265 $tpl->setVariable("DEPENDANT_GROUP", $dependant_group_html); 266 } 267 268 269 return $tpl->get(); 270 } 271 272 /** 273 * Escape values for rendering. 274 * In order to prevent XSS-attacks, values need to be stripped of 275 * special chars (such as quotes or tags). 276 * Needs vary according to the type of component (e.g. the html 277 * generated for this component and the placement of {VALUE} in the 278 * template.) 279 * Please note: this may not work for customized templates! 280 */ 281 protected function prepareValue(Input $component, $value) 282 { 283 switch (true) { 284 case ($component instanceof Textarea): 285 return htmlentities($value); 286 case ($component instanceof Text): 287 case ($component instanceof Password): 288 case ($component instanceof Numeric): 289 return htmlspecialchars($value, ENT_QUOTES); 290 default: 291 return $value; 292 } 293 } 294 295 /** 296 * @param Template $tpl 297 * @param Input $input 298 * @param $id 299 * @param RendererInterface $default_renderer 300 * 301 * @return string 302 */ 303 protected function renderInputField(Template $tpl, Input $input, $id, RendererInterface $default_renderer) 304 { 305 $input = $this->setSignals($input); 306 307 if ($input instanceof Component\Input\Field\Password) { 308 $id = $this->additionalRenderPassword($tpl, $input, $default_renderer); 309 } 310 311 if ($input instanceof Textarea) { 312 $this->renderTextareaField($tpl, $input); 313 } 314 315 $tpl->setVariable("NAME", $input->getName()); 316 317 switch (true) { 318 case ($input instanceof Checkbox || $input instanceof OptionalGroup): 319 if ($input->getValue()) { 320 $tpl->touchBlock("value"); 321 } 322 // no break 323 case ($input instanceof Text): 324 case ($input instanceof Numeric): 325 case ($input instanceof Password): 326 case ($input instanceof Textarea): 327 $tpl->setVariable("NAME", $input->getName()); 328 329 if ($input->getValue() !== null && !($input instanceof Checkbox)) { 330 $tpl->setCurrentBlock("value"); 331 /* 332 ATTENTION! Please see docs of "prepareValue". 333 */ 334 $tpl->setVariable("VALUE", $this->prepareValue($input, $input->getValue())); 335 $tpl->parseCurrentBlock(); 336 } 337 if ($input->isDisabled()) { 338 $tpl->setCurrentBlock("disabled"); 339 $tpl->setVariable("DISABLED", 'disabled="disabled"'); 340 $tpl->parseCurrentBlock(); 341 } 342 if ($id) { 343 $tpl->setCurrentBlock("id"); 344 $tpl->setVariable("ID", $id); 345 $tpl->parseCurrentBlock(); 346 } 347 break; 348 case ($input instanceof Select): 349 $tpl = $this->renderSelectInput($tpl, $input); 350 break; 351 case ($input instanceof MultiSelect): 352 $tpl = $this->renderMultiSelectInput($tpl, $input); 353 break; 354 355 case ($input instanceof Tag): 356 $configuration = $input->getConfiguration(); 357 $input = $input->withAdditionalOnLoadCode( 358 function ($id) use ($configuration) { 359 $encoded = json_encode($configuration); 360 361 return "il.UI.Input.tagInput.init('{$id}', {$encoded});"; 362 } 363 ); 364 $id = $this->bindJavaScript($input); 365 /** 366 * @var $input \ILIAS\UI\Implementation\Component\Input\Field\Tag 367 */ 368 $tpl->setVariable("ID", $id); 369 $tpl->setVariable("NAME", $input->getName()); 370 if ($input->isDisabled()) { 371 $tpl->setCurrentBlock("disabled"); 372 $tpl->setVariable("DISABLED", "disabled"); 373 $tpl->parseCurrentBlock(); 374 } 375 if ($input->getValue()) { 376 $value = $input->getValue(); 377 $tpl->setVariable("VALUE_COMMA_SEPARATED", implode(",", $value)); 378 foreach ($value as $tag) { 379 $tpl->setCurrentBlock('existing_tags'); 380 $tpl->setVariable("FIELD_ID", $id); 381 $tpl->setVariable("FIELD_NAME", $input->getName()); 382 $tpl->setVariable("TAG_NAME", $tag); 383 $tpl->parseCurrentBlock(); 384 } 385 } 386 break; 387 case ($input instanceof DateTime): 388 return $this->renderDateTimeInput($default_renderer, $tpl, $input); 389 break; 390 case ($input instanceof Component\Input\Field\File): 391 $input = $this->renderFileInput($input); 392 break; 393 } 394 395 if ($id === null) { 396 $this->maybeRenderId($input, $tpl); 397 } 398 399 return $tpl->get(); 400 } 401 402 public function renderSelectInput(Template $tpl, Select $input) 403 { 404 if ($input->isDisabled()) { 405 $tpl->setCurrentBlock("disabled"); 406 $tpl->setVariable("DISABLED", 'disabled="disabled"'); 407 $tpl->parseCurrentBlock(); 408 } 409 $value = $input->getValue(); 410 //disable first option if required. 411 $tpl->setCurrentBlock("options"); 412 if (!$value) { 413 $tpl->setVariable("SELECTED", "selected"); 414 } 415 if ($input->isRequired()) { 416 $tpl->setVariable("DISABLED_OPTION", "disabled"); 417 $tpl->setVariable("HIDDEN", "hidden"); 418 } 419 $tpl->setVariable("VALUE", null); 420 $tpl->setVariable("VALUE_STR", "-"); 421 $tpl->parseCurrentBlock(); 422 //rest of options. 423 foreach ($input->getOptions() as $option_key => $option_value) { 424 $tpl->setCurrentBlock("options"); 425 if ($value == $option_key) { 426 $tpl->setVariable("SELECTED", "selected"); 427 } 428 $tpl->setVariable("VALUE", $option_key); 429 $tpl->setVariable("VALUE_STR", $option_value); 430 $tpl->parseCurrentBlock(); 431 } 432 433 return $tpl; 434 } 435 436 public function renderMultiSelectInput(Template $tpl, MultiSelect $input) : Template 437 { 438 $value = $input->getValue(); 439 $name = $input->getName(); 440 441 foreach ($input->getOptions() as $opt_value => $opt_label) { 442 $tpl->setCurrentBlock("option"); 443 $tpl->setVariable("NAME", $name); 444 $tpl->setVariable("VALUE", $opt_value); 445 $tpl->setVariable("LABEL", $opt_label); 446 447 if ($value && in_array($opt_value, $value)) { 448 $tpl->setVariable("CHECKED", 'checked="checked"'); 449 } 450 if ($input->isDisabled()) { 451 $tpl->setVariable("DISABLED", 'disabled="disabled"'); 452 } 453 454 $tpl->parseCurrentBlock(); 455 } 456 return $tpl; 457 } 458 459 460 /** 461 * Render revelation-glyphs for password and register signals/functions 462 * @param Template $tpl 463 * @param Password $input 464 * 465 * @return string | false 466 */ 467 protected function additionalRenderPassword(Template $tpl, Component\Input\Field\Password $input, RendererInterface $default_renderer) 468 { 469 $id = null; 470 if ($input->getRevelation()) { 471 $f = $this->getUIFactory(); 472 473 $input = $input->withResetSignals(); 474 $sig_reveal = $input->getRevealSignal(); 475 $sig_mask = $input->getMaskSignal(); 476 477 $input = $input->withAdditionalOnLoadCode(function ($id) use ($sig_reveal, $sig_mask) { 478 return 479 "$(document).on('{$sig_reveal}', function() { 480 $('#{$id}').addClass('revealed'); 481 $('#{$id}')[0].getElementsByTagName('input')[0].type='text'; 482 });" . 483 "$(document).on('{$sig_mask}', function() { 484 $('#{$id}').removeClass('revealed'); 485 $('#{$id}')[0].getElementsByTagName('input')[0].type='password'; 486 });" 487 ; 488 }); 489 $id = $this->bindJavaScript($input); 490 $tpl->setVariable("ID", $id); 491 492 $glyph_reveal = $f->symbol()->glyph()->eyeopen("#") 493 ->withOnClick($sig_reveal); 494 $glyph_mask = $f->symbol()->glyph()->eyeclosed("#") 495 ->withOnClick($sig_mask); 496 $tpl->setCurrentBlock('revelation'); 497 $tpl->setVariable('PASSWORD_REVEAL', $default_renderer->render($glyph_reveal)); 498 $tpl->setVariable('PASSWORD_MASK', $default_renderer->render($glyph_mask)); 499 $tpl->parseCurrentBlock(); 500 } 501 return $id; 502 } 503 504 505 protected function renderTextareaField(Template $tpl, Textarea $input) 506 { 507 if ($input->isLimited()) { 508 $this->toJS("ui_chars_remaining"); 509 $this->toJS("ui_chars_min"); 510 $this->toJS("ui_chars_max"); 511 512 $counter_id_prefix = "textarea_feedback_"; 513 $min = $input->getMinLimit(); 514 $max = $input->getMaxLimit(); 515 516 $input = $input->withAdditionalOnLoadCode(function ($id) use ($counter_id_prefix, $min, $max) { 517 return "il.UI.textarea.changeCounter('$id','$counter_id_prefix','$min','$max');"; 518 }); 519 520 $textarea_id = $this->bindJavaScript($input); 521 $tpl->setCurrentBlock("id"); 522 $tpl->setVariable("ID", $textarea_id); 523 $tpl->parseCurrentBlock(); 524 $tpl->setCurrentBlock("limit"); 525 $tpl->setVariable("COUNT_ID", $textarea_id); 526 $tpl->setVariable("FEEDBACK_MAX_LIMIT", $max); 527 $tpl->parseCurrentBlock(); 528 } 529 } 530 531 532 /** 533 * @param Radio $input 534 * @param RendererInterface $default_renderer 535 * 536 * @return string 537 */ 538 protected function renderRadioField(Component\Input\Field\Radio $input, RendererInterface $default_renderer) 539 { 540 $input_tpl = $this->getTemplate("tpl.radio.html", true, true); 541 542 //monitor change-events 543 $input = $this->setSignals($input); 544 $id = $this->bindJavaScript($input) ?? $this->createId(); 545 $input_tpl->setVariable("ID", $id); 546 547 foreach ($input->getOptions() as $value => $label) { 548 $opt_id = $id . '_' . $value . '_opt'; 549 550 $input_tpl->setCurrentBlock('optionblock'); 551 $input_tpl->setVariable("NAME", $input->getName()); 552 $input_tpl->setVariable("OPTIONID", $opt_id); 553 $input_tpl->setVariable("VALUE", $value); 554 $input_tpl->setVariable("LABEL", $label); 555 556 if ($input->getValue() !== null && $input->getValue() == $value) { 557 $input_tpl->setVariable("CHECKED", 'checked="checked"'); 558 } 559 if ($input->isDisabled()) { 560 $input_tpl->setVariable("DISABLED", 'disabled="disabled"'); 561 } 562 563 $byline = $input->getBylineFor($value); 564 if (!empty($byline)) { 565 $input_tpl->setVariable("BYLINE", $byline); 566 } 567 568 $input_tpl->parseCurrentBlock(); 569 } 570 $options_html = $input_tpl->get(); 571 572 //render with context: 573 $tpl = $this->getTemplate("tpl.context_form.html", true, true); 574 $tpl->setVariable("LABEL", $input->getLabel()); 575 $tpl->setVariable("INPUT", $options_html); 576 577 if ($input->getByline() !== null) { 578 $tpl->setCurrentBlock("byline"); 579 $tpl->setVariable("BYLINE", $input->getByline()); 580 $tpl->parseCurrentBlock(); 581 } 582 if ($input->isRequired()) { 583 $tpl->touchBlock("required"); 584 } 585 if ($input->getError() !== null) { 586 $tpl->setCurrentBlock("error"); 587 $tpl->setVariable("ERROR", $input->getError()); 588 $tpl->parseCurrentBlock(); 589 } 590 return $tpl->get(); 591 } 592 593 /** 594 * @param Radio $input 595 * @param RendererInterface $default_renderer 596 * 597 * @return string 598 */ 599 protected function renderSwitchableGroupField(Component\Input\Field\SwitchableGroup $input, RendererInterface $default_renderer) 600 { 601 $input_tpl = $this->getTemplate("tpl.radio.html", true, true); 602 603 $input = $input->withAdditionalOnLoadCode(function ($id) { 604 return $this->getSwitchableGroupOnLoadCode($id); 605 }); 606 $id = $this->bindJavaScript($input); 607 $input_tpl->setVariable("ID", $id); 608 609 foreach ($input->getInputs() as $key => $group) { 610 $opt_id = $id . '_' . $key . '_opt'; 611 612 $input_tpl->setCurrentBlock('optionblock'); 613 $input_tpl->setVariable("NAME", $input->getName()); 614 $input_tpl->setVariable("OPTIONID", $opt_id); 615 $input_tpl->setVariable("VALUE", $key); 616 $input_tpl->setVariable("LABEL", $group->getLabel()); 617 618 if ($input->getValue() !== null) { 619 list($index, $subvalues) = $input->getValue(); 620 if ($index == $key) { 621 $input_tpl->setVariable("CHECKED", 'checked="checked"'); 622 } 623 } 624 if ($input->isDisabled()) { 625 $input_tpl->setVariable("DISABLED", 'disabled="disabled"'); 626 } 627 628 $dependant_group_html = $this->renderFieldGroups($group, $default_renderer); 629 $input_tpl->setVariable("DEPENDANT_FIELDS", $dependant_group_html); 630 $input_tpl->parseCurrentBlock(); 631 } 632 $options_html = $input_tpl->get(); 633 634 //render with context: 635 $tpl = $this->getTemplate("tpl.context_form.html", true, true); 636 $tpl->setVariable("LABEL", $input->getLabel()); 637 $tpl->setVariable("INPUT", $options_html); 638 639 if ($input->getByline() !== null) { 640 $tpl->setCurrentBlock("byline"); 641 $tpl->setVariable("BYLINE", $input->getByline()); 642 $tpl->parseCurrentBlock(); 643 } 644 if ($input->isRequired()) { 645 $tpl->touchBlock("required"); 646 } 647 if ($input->getError() !== null) { 648 $tpl->setCurrentBlock("error"); 649 $tpl->setVariable("ERROR", $input->getError()); 650 $tpl->parseCurrentBlock(); 651 } 652 return $tpl->get(); 653 } 654 655 656 protected function getOptionalGroupOnLoadCode($id) 657 { 658 return <<<JS 659var $id = $("#$id"); 660var {$id}_group = $id.siblings(".form-group").show(); 661var {$id}_adjust = function() { 662 if ({$id}[0].checked) { 663 {$id}_group.show(); 664 } 665 else { 666 {$id}_group.hide() 667 } 668} 669$id.change({$id}_adjust); 670{$id}_adjust(); 671JS; 672 } 673 674 protected function getSwitchableGroupOnLoadCode($id) 675 { 676 return <<<JS 677var radio = $("#$id"); 678radio.change(function(event){ 679 var r = $(this), 680 options = r.children('.il-input-radiooption').children('input'); 681 682 options.each(function(index, opt) { 683 var group = $(opt).siblings('.form-group'); 684 if(opt.checked) { 685 group.show(); 686 } else { 687 group.hide(); 688 } 689 }); 690}); 691radio.trigger('change'); 692 693JS; 694 } 695 696 /** 697 * Return the datetime format in a form fit for the JS-component of this input. 698 * Currently, this means transforming the elements of DateFormat to momentjs. 699 * 700 * http://eonasdan.github.io/bootstrap-datetimepicker/Options/#format 701 * http://momentjs.com/docs/#/displaying/format/ 702 */ 703 protected function getTransformedDateFormat( 704 DateFormat\DateFormat $origin, 705 array $mapping 706 ) : string { 707 $ret = ''; 708 foreach ($origin->toArray() as $element) { 709 if (array_key_exists($element, $mapping)) { 710 $ret .= $mapping[$element]; 711 } else { 712 $ret .= $element; 713 } 714 } 715 return $ret; 716 } 717 718 /** 719 * @param Template $tpl 720 * @param DateTime $input 721 * 722 * @return string 723 */ 724 protected function renderDateTimeInput(RendererInterface $default_renderer, Template $tpl, DateTime $input) : string 725 { 726 $f = $this->getUIFactory(); 727 728 if ($input->getTimeOnly() === true) { 729 $cal_glyph = $f->symbol()->glyph()->time("#"); 730 $format = $input::TIME_FORMAT; 731 } else { 732 $cal_glyph = $f->symbol()->glyph()->calendar("#"); 733 734 $format = $this->getTransformedDateFormat( 735 $input->getFormat(), 736 self::DATEPICKER_FORMAT_MAPPING 737 ); 738 739 if ($input->getUseTime() === true) { 740 $format .= ' ' . $input::TIME_FORMAT; 741 } 742 } 743 744 $tpl->setVariable("CALENDAR_GLYPH", $default_renderer->render($cal_glyph)); 745 746 $config = [ 747 'showClear' => true, 748 'sideBySide' => true, 749 'format' => $format, 750 ]; 751 $config = array_merge($config, $input->getAdditionalPickerConfig()); 752 753 $min_date = $input->getMinValue(); 754 if (!is_null($min_date)) { 755 $config['minDate'] = date_format($min_date, self::DATEPICKER_MINMAX_FORMAT); 756 } 757 $max_date = $input->getMaxValue(); 758 if (!is_null($max_date)) { 759 $config['maxDate'] = date_format($max_date, self::DATEPICKER_MINMAX_FORMAT); 760 } 761 762 $tpl->setVariable("NAME", $input->getName()); 763 $tpl->setVariable("PLACEHOLDER", $format); 764 765 if ($input->getValue() !== null) { 766 $tpl->setCurrentBlock("value"); 767 $tpl->setVariable("VALUE", $input->getValue()); 768 $tpl->parseCurrentBlock(); 769 } 770 771 require_once("./Services/Calendar/classes/class.ilCalendarUtil.php"); 772 \ilCalendarUtil::initDateTimePicker(); 773 $input = $this->setSignals($input); 774 775 $disabled = $input->isDisabled(); 776 $input = $input->withAdditionalOnLoadCode(function ($id) use ($config, $disabled) { 777 $js = '$("#' . $id . '").datetimepicker(' . json_encode($config) . ');'; 778 if ($disabled) { 779 $js .= '$("#' . $id . ' input").prop(\'disabled\', true);'; 780 } 781 return $js; 782 }); 783 784 $id = $this->bindJavaScript($input); 785 $tpl->setVariable("ID", $id); 786 787 return $tpl->get(); 788 } 789 790 791 protected function renderDurationInput(Duration $input, RendererInterface $default_renderer) : string 792 { 793 $tpl = $this->getTemplate("tpl.context_form.html", true, true); 794 $tpl_duration = $this->getTemplate("tpl.duration.html", true, true); 795 796 if ($input->getName()) { 797 $tpl->setVariable("NAME", $input->getName()); 798 } else { 799 $tpl->setVariable("NAME", ""); 800 } 801 802 $tpl->setVariable("LABEL", $input->getLabel()); 803 804 if ($input->getByline() !== null) { 805 $tpl->setCurrentBlock("byline"); 806 $tpl->setVariable("BYLINE", $input->getByline()); 807 $tpl->parseCurrentBlock(); 808 } 809 810 if ($input->isRequired()) { 811 $tpl->touchBlock("required"); 812 } 813 814 if ($input->getError() !== null) { 815 $tpl->setCurrentBlock("error"); 816 $tpl->setVariable("ERROR", $input->getError()); 817 $tpl->parseCurrentBlock(); 818 } 819 820 $input = $this->setSignals($input); 821 $input = $input->withAdditionalOnLoadCode( 822 function ($id) { 823 return "$(document).ready(function() { 824 il.UI.Input.duration.init('$id'); 825 });"; 826 } 827 ); 828 $id = $this->bindJavaScript($input); 829 $tpl_duration->setVariable("ID", $id); 830 831 $input_html = ''; 832 $inputs = $input->getInputs(); 833 834 $inpt = array_shift($inputs); //from 835 $input_html .= $default_renderer->render($inpt); 836 837 $inpt = array_shift($inputs)->withAdditionalPickerconfig([ //until 838 'useCurrent' => false 839 ]); 840 $input_html .= $default_renderer->render($inpt); 841 842 $tpl_duration->setVariable('DURATION', $input_html); 843 $tpl->setVariable("INPUT", $tpl_duration->get()); 844 return $tpl->get(); 845 } 846 847 /** 848 * @inheritdoc 849 */ 850 protected function getComponentInterfaceName() 851 { 852 return [ 853 Component\Input\Field\Text::class, 854 Component\Input\Field\Numeric::class, 855 Component\Input\Field\Group::class, 856 Component\Input\Field\OptionalGroup::class, 857 Component\Input\Field\SwitchableGroup::class, 858 Component\Input\Field\Section::class, 859 Component\Input\Field\Checkbox::class, 860 Component\Input\Field\Tag::class, 861 Component\Input\Field\Password::class, 862 Component\Input\Field\Select::class, 863 Component\Input\Field\Radio::class, 864 Component\Input\Field\Textarea::class, 865 Component\Input\Field\MultiSelect::class, 866 Component\Input\Field\DateTime::class, 867 Component\Input\Field\Duration::class, 868 Component\Input\Field\File::class 869 ]; 870 } 871 872 873 protected function renderFileInput(Component\Input\Field\File $input) : Component\Input\Field\File 874 { 875 $component = $this->setSignals($input); 876 /** 877 * @var $component File 878 */ 879 $settings = new \stdClass(); 880 $settings->upload_url = $component->getUploadHandler()->getUploadURL(); 881 $settings->removal_url = $component->getUploadHandler()->getFileRemovalURL(); 882 $settings->info_url = $component->getUploadHandler()->getExistingFileInfoURL(); 883 $settings->file_identifier_key = $component->getUploadHandler()->getFileIdentifierParameterName(); 884 $settings->accepted_files = implode(',', $component->getAcceptedMimeTypes()); 885 $settings->existing_file_ids = $input->getValue(); 886 $settings->existing_files = $component->getUploadHandler()->getInfoForExistingFiles($input->getValue() ?? []); 887 $settings->dictInvalidFileType = $this->txt('form_msg_file_wrong_file_type'); 888 889 $input = $component->withAdditionalOnLoadCode( 890 function ($id) use ($settings) { 891 $settings = json_encode($settings); 892 893 return "$(document).ready(function() { 894 il.UI.Input.file.init('$id', '{$settings}'); 895 });"; 896 } 897 ); 898 899 /** 900 * @var $input Component\Input\Field\File 901 */ 902 return $input; 903 } 904} 905