From 581760accc19a504c864d9e4cf1af570a3984c48 Mon Sep 17 00:00:00 2001 From: Tim Schmitz Date: Fri, 19 Dec 2025 15:01:45 +0100 Subject: [PATCH 1/2] UI: show values selected in complex filter input fields (46588) --- .../Component/Input/Field/Duration.php | 14 +++++++++++++- .../Input/Field/FilterContextRenderer.php | 6 +++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/components/ILIAS/UI/src/Implementation/Component/Input/Field/Duration.php b/components/ILIAS/UI/src/Implementation/Component/Input/Field/Duration.php index 5aca3ece6537..43d940ffe278 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Input/Field/Duration.php +++ b/components/ILIAS/UI/src/Implementation/Component/Input/Field/Duration.php @@ -301,7 +301,19 @@ public function getUpdateOnLoadCode(): Closure return fn($id) => "var combinedDuration = function() { var options = []; $('#$id').find('input').each(function() { - options.push($(this).val()); + const value = $(this).val(); + if (value == '') { + options.push(''); + return; + } + const date = new Date(value); + var readable_value = ''; + if (value.includes('T')) { + readable_value = date.toLocaleString([], { dateStyle: 'short', timeStyle: 'short' }); + } else { + readable_value = date.toLocaleDateString(); + } + options.push(readable_value); }); return options.join(' - '); } diff --git a/components/ILIAS/UI/src/Implementation/Component/Input/Field/FilterContextRenderer.php b/components/ILIAS/UI/src/Implementation/Component/Input/Field/FilterContextRenderer.php index f65d35f0a9a2..5067cd338290 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Input/Field/FilterContextRenderer.php +++ b/components/ILIAS/UI/src/Implementation/Component/Input/Field/FilterContextRenderer.php @@ -158,7 +158,7 @@ protected function wrapInFilterContext( $tpl->parseCurrentBlock(); $tpl->setCurrentBlock("filter_field"); if ($component->isComplex()) { - $tpl->setVariable("FILTER_FIELD", $this->renderProxyField($input_html, $default_renderer)); + $tpl->setVariable("FILTER_FIELD", $this->renderProxyField($component, $input_html, $default_renderer)); } else { $tpl->setVariable("FILTER_FIELD", $input_html); } @@ -176,6 +176,7 @@ protected function maybeDisable(FormInput $component, Template $tpl): void } protected function renderProxyField( + FormInput $component, string $input_html, RendererInterface $default_renderer ): string { @@ -183,6 +184,9 @@ protected function renderProxyField( $tpl = $this->getTemplate("tpl.filter_field.html", true, true); $popover = $f->popover()->standard($f->legacy($input_html))->withVerticalPosition(); + if ($component->getOnLoadCode() !== null) { + $popover = $popover->withAdditionalOnLoadCode($component->getOnLoadCode()); + } $tpl->setVariable("POPOVER", $default_renderer->render($popover)); $prox = new ProxyFilterField(); From 5c8e5ec1b19f6fb338220cf03becbc38e7c4fee9 Mon Sep 17 00:00:00 2001 From: Thibeau Fuhrer Date: Tue, 10 Feb 2026 11:44:40 +0100 Subject: [PATCH 2/2] [FIX] #46588 UI: `Input\Container\Filter\FilterInput` JavaScript binding * Fixes https://mantis.ilias.de/view.php?id=46588 * Update HTML of filter input context * Update rendering unit tests * Add new filter example --- .../Component/Input/Field/Duration.php | 48 +++---- .../Input/Field/FilterContextRenderer.php | 12 +- .../Component/Input/Field/MultiSelect.php | 34 ++--- .../Standard/with_additional_on_load_code.php | 57 ++++++++ .../default/Input/tpl.context_filter.html | 2 +- components/ILIAS/UI/tests/Base.php | 5 + .../Container/Filter/FilterInputTest.php | 25 ++-- .../Container/Filter/StandardFilterTest.php | 124 +++++++++--------- 8 files changed, 188 insertions(+), 119 deletions(-) create mode 100755 components/ILIAS/UI/src/examples/Input/Container/Filter/Standard/with_additional_on_load_code.php diff --git a/components/ILIAS/UI/src/Implementation/Component/Input/Field/Duration.php b/components/ILIAS/UI/src/Implementation/Component/Input/Field/Duration.php index 43d940ffe278..999f49a1af02 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Input/Field/Duration.php +++ b/components/ILIAS/UI/src/Implementation/Component/Input/Field/Duration.php @@ -298,29 +298,31 @@ protected function getConstraintForRequirement(): ?Constraint */ public function getUpdateOnLoadCode(): Closure { - return fn($id) => "var combinedDuration = function() { - var options = []; - $('#$id').find('input').each(function() { - const value = $(this).val(); - if (value == '') { - options.push(''); - return; - } - const date = new Date(value); - var readable_value = ''; - if (value.includes('T')) { - readable_value = date.toLocaleString([], { dateStyle: 'short', timeStyle: 'short' }); - } else { - readable_value = date.toLocaleDateString(); - } - options.push(readable_value); - }); - return options.join(' - '); - } - $('#$id').on('input', function(event) { - il.UI.input.onFieldUpdate(event, '$id', combinedDuration()); - }); - il.UI.input.onFieldUpdate(event, '$id', combinedDuration());"; + return static fn($id) => << (input.value) ? formatDateTimeValue(input.value) : '') + .join(' - '); + } + const durationField = document.getElementById('$id'); + const dateTimeInputs = durationField.querySelectorAll('.c-field-datetime'); + dateTimeInputs.forEach((input) => { + input.addEventListener('input', (event) => { + il.UI.input.onFieldUpdate(event, '$id', reduceDateTimeInputs(dateTimeInputs)); + }); + }); + il.UI.input.onFieldUpdate(undefined, '$id', reduceDateTimeInputs(dateTimeInputs)); + })(); +JS; } public function withLabels(string $start_label, string $end_label): C\Input\Field\Duration diff --git a/components/ILIAS/UI/src/Implementation/Component/Input/Field/FilterContextRenderer.php b/components/ILIAS/UI/src/Implementation/Component/Input/Field/FilterContextRenderer.php index 5067cd338290..143d29bbf7e7 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Input/Field/FilterContextRenderer.php +++ b/components/ILIAS/UI/src/Implementation/Component/Input/Field/FilterContextRenderer.php @@ -148,6 +148,14 @@ protected function wrapInFilterContext( return false; // stop event propagation });"); + $tpl->setVariable("UI_COMPONENT_NAME", $this->getComponentCanonicalNameAttribute($component)); + $tpl->setVariable("INPUT_NAME", $component->getName()); + + if ($component->getOnLoadCode() !== null) { + $binding_id = $this->bindJavaScript($component) ?? $this->createId(); + $tpl->setVariable("BINDING_ID", $binding_id); + } + $tpl->setCurrentBlock("addon_left"); $tpl->setVariable("LABEL", $component->getLabel()); if ($id_pointing_to_input) { @@ -184,9 +192,6 @@ protected function renderProxyField( $tpl = $this->getTemplate("tpl.filter_field.html", true, true); $popover = $f->popover()->standard($f->legacy($input_html))->withVerticalPosition(); - if ($component->getOnLoadCode() !== null) { - $popover = $popover->withAdditionalOnLoadCode($component->getOnLoadCode()); - } $tpl->setVariable("POPOVER", $default_renderer->render($popover)); $prox = new ProxyFilterField(); @@ -211,7 +216,6 @@ protected function renderDurationField(F\Duration $component, RendererInterface $input_html .= $default_renderer->render($input); $tpl = $this->getTemplate("tpl.duration.html", true, true); - $id = $this->bindJSandApplyId($component, $tpl); $tpl->setVariable('DURATION', $input_html); return $this->wrapInFormContext($component, $component->getLabel(), $tpl->get()); diff --git a/components/ILIAS/UI/src/Implementation/Component/Input/Field/MultiSelect.php b/components/ILIAS/UI/src/Implementation/Component/Input/Field/MultiSelect.php index 0093b299c1b8..8b34607c20fb 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Input/Field/MultiSelect.php +++ b/components/ILIAS/UI/src/Implementation/Component/Input/Field/MultiSelect.php @@ -96,21 +96,25 @@ protected function getConstraintForRequirement(): ?Constraint */ public function getUpdateOnLoadCode(): Closure { - return fn($id) => "(function() { - var checkedBoxes = function() { - var options = []; - $('#$id').find('li').each(function() { - if ($(this).find('input').prop('checked')) { - options.push($(this).find('span').text()); - } - }); - return options.join(', '); - } - $('#$id').on('input', function(event) { - il.UI.input.onFieldUpdate(event, '$id', checkedBoxes()); - }); - il.UI.input.onFieldUpdate(event, '$id', checkedBoxes()); - })();"; + return static fn($id) => << input.checked) + .map((input) => input.parentElement.querySelector('.c-field-multiselect__label-text')?.textContent ?? '') + .join(', '); + } + const multiSelectField = document.getElementById('$id'); + const multiSelectCheckboxInputs = multiSelectField.querySelectorAll('.c-field-multiselect input[type="checkbox"]'); + multiSelectCheckboxInputs.forEach((input) => { + input.addEventListener('input', (event) => { + il.UI.input.onFieldUpdate(event, '$id', reduceMultiSelectCheckboxInputs(multiSelectCheckboxInputs)); + }); + }); + il.UI.input.onFieldUpdate(undefined, '$id', reduceMultiSelectCheckboxInputs(multiSelectCheckboxInputs)); + })(); +JS; } /** diff --git a/components/ILIAS/UI/src/examples/Input/Container/Filter/Standard/with_additional_on_load_code.php b/components/ILIAS/UI/src/examples/Input/Container/Filter/Standard/with_additional_on_load_code.php new file mode 100755 index 000000000000..287323978725 --- /dev/null +++ b/components/ILIAS/UI/src/examples/Input/Container/Filter/Standard/with_additional_on_load_code.php @@ -0,0 +1,57 @@ + + * Example showing a Filter Container and Filter Inputs with additional JavaScript on-load-code + * attached to them. + * + * expected output: > + * ILIAS shows the rendered Filter Component with several Filter Inputs. When opening the browser + * console, for each of the Filter Input, as well as the Filter Container, a log entry that refers + * to their ID will be visible. + * --- + */ +function with_additional_on_load_code(): string +{ + global $DIC; + + $factory = $DIC->ui()->factory(); + $renderer = $DIC->ui()->renderer(); + + $pseudo_load_code = static fn($name) => static fn($id) => "console.log('Loaded $name with ID: ' + '$id');"; + + $pseudo_options = [ + 'A' => 'Option A', + 'B' => 'Option B', + 'C' => 'Option C', + ]; + + $filter_inputs = [ + $factory->input()->field()->multiSelect('multi-select', $pseudo_options)->withAdditionalOnLoadCode($pseudo_load_code('multi-select')), + $factory->input()->field()->select('single-select', $pseudo_options)->withAdditionalOnLoadCode($pseudo_load_code('single-select')), + $factory->input()->field()->duration('duration')->withAdditionalOnLoadCode($pseudo_load_code('duration')), + $factory->input()->field()->dateTime('datetime')->withAdditionalOnLoadCode($pseudo_load_code('datetime')), + $factory->input()->field()->numeric('numeric')->withAdditionalOnLoadCode($pseudo_load_code('numeric')), + $factory->input()->field()->text('text')->withAdditionalOnLoadCode($pseudo_load_code('text')), + ]; + + $filter = $factory->input()->container()->filter()->standard( + '#', + '#', + '#', + '#', + '#', + '#', + $filter_inputs, + array_map(static fn() => true, $filter_inputs), + true, + true, + )->withAdditionalOnLoadCode($pseudo_load_code('filter')); + + return $renderer->render($filter); +} diff --git a/components/ILIAS/UI/src/templates/default/Input/tpl.context_filter.html b/components/ILIAS/UI/src/templates/default/Input/tpl.context_filter.html index fa7405c79101..5b7ec1eab21a 100755 --- a/components/ILIAS/UI/src/templates/default/Input/tpl.context_filter.html +++ b/components/ILIAS/UI/src/templates/default/Input/tpl.context_filter.html @@ -1,5 +1,5 @@
-
+
id="{BINDING_ID}" class="input-group"> diff --git a/components/ILIAS/UI/tests/Base.php b/components/ILIAS/UI/tests/Base.php index 72cd090f4997..22097a021ed1 100755 --- a/components/ILIAS/UI/tests/Base.php +++ b/components/ILIAS/UI/tests/Base.php @@ -480,6 +480,11 @@ public function normalizeHTML(string $html): string return trim(str_replace(["\n", "\r"], "", $html)); } + /** + * @deprecated please try not to use this method anymore. It relies on DOMDocument, which DOES NOT + * support HTML5 and will trigger warnings for such elements and COULD add self-closing + * tags ( <... />) automatically, which produces false-positives. + */ public function assertHTMLEquals(string $expected_html_as_string, string $html_as_string): void { $html = new DOMDocument(); diff --git a/components/ILIAS/UI/tests/Component/Input/Container/Filter/FilterInputTest.php b/components/ILIAS/UI/tests/Component/Input/Container/Filter/FilterInputTest.php index c24229aa3553..4320b19fe68c 100644 --- a/components/ILIAS/UI/tests/Component/Input/Container/Filter/FilterInputTest.php +++ b/components/ILIAS/UI/tests/Component/Input/Container/Filter/FilterInputTest.php @@ -123,7 +123,7 @@ public function testRenderTextWithFilterContext(): void $expected = $this->brutallyTrimHTML('
-
+
@@ -147,8 +147,7 @@ public function testRenderNumericWithFilterContext(): void $html = $this->brutallyTrimHTML($fr->render($numeric)); $expected = $this->brutallyTrimHTML(' -
-
+
@@ -173,8 +172,7 @@ public function testRenderSelectWithFilterContext(): void $html = $this->brutallyTrimHTML($fr->render($select)); $expected = $this->brutallyTrimHTML(' -
-
+
@@ -283,18 +278,16 @@ public function testRenderDurationWithFilterContext(): void $expected = $this->brutallyTrimHTML(' -
-
+
- - + + - +
- {POPOVER}
'); $this->assertHTMLEquals($expected, $html); diff --git a/components/ILIAS/UI/tests/Component/Input/Container/Filter/StandardFilterTest.php b/components/ILIAS/UI/tests/Component/Input/Container/Filter/StandardFilterTest.php index 19e33c6d59e7..e93fba92c60b 100755 --- a/components/ILIAS/UI/tests/Component/Input/Container/Filter/StandardFilterTest.php +++ b/components/ILIAS/UI/tests/Component/Input/Container/Filter/StandardFilterTest.php @@ -183,7 +183,8 @@ public function testRenderActivatedCollapsed(): void - filter + + filter
@@ -204,54 +205,54 @@ public function testRenderActivatedCollapsed(): void
-
+
- +
-
- - - +
-
+
- - + + - +
-
-
- +
EOT; - $this->assertHTMLEquals($this->brutallyTrimHTML($expected), $this->brutallyTrimHTML($html)); + $this->assertEquals($this->brutallyTrimHTML($expected), $this->brutallyTrimHTML($html)); } public function testRenderDeactivatedCollapsed(): void @@ -321,7 +322,8 @@ public function testRenderDeactivatedCollapsed(): void - filter + + filter
@@ -342,54 +344,54 @@ public function testRenderDeactivatedCollapsed(): void
-
+
- +
-
- - - +
-
+
- - + + - +
-
-
- +
EOT; - $this->assertHTMLEquals($this->brutallyTrimHTML($expected), $this->brutallyTrimHTML($html)); + $this->assertEquals($this->brutallyTrimHTML($expected), $this->brutallyTrimHTML($html)); } public function testRenderActivatedExpanded(): void @@ -459,7 +461,8 @@ public function testRenderActivatedExpanded(): void - filter + + filter
@@ -480,54 +483,54 @@ public function testRenderActivatedExpanded(): void
-
+
- +
-
- - - +
-
+
- - + + - +
-
-
- +
EOT; - $this->assertHTMLEquals($this->brutallyTrimHTML($expected), $this->brutallyTrimHTML($html)); + $this->assertEquals($this->brutallyTrimHTML($expected), $this->brutallyTrimHTML($html)); } public function testRenderDeactivatedExpanded(): void @@ -597,7 +600,8 @@ public function testRenderDeactivatedExpanded(): void - filter + + filter
@@ -618,54 +622,54 @@ public function testRenderDeactivatedExpanded(): void
-
+
- +
-
- - - +
-
+
- - + + - +
-
-
- +
EOT; - $this->assertHTMLEquals($this->brutallyTrimHTML($expected), $this->brutallyTrimHTML($html)); + $this->assertEquals($this->brutallyTrimHTML($expected), $this->brutallyTrimHTML($html)); } public function testDedicatedNames(): void