From 2fea295ea5bc01f289a777efd12bc8a10b40b3af Mon Sep 17 00:00:00 2001 From: Fabian Helfer Date: Thu, 8 Jan 2026 11:17:38 +0100 Subject: [PATCH 1/3] [Fix] #0045447 UI: Add new preventSepcialChar Mehtods o Numeric Input in OderingTable --- .../UI/resources/js/Input/Field/input.js | 3 + .../UI/src/Component/Input/Field/Numeric.php | 40 ++++ .../Component/Input/Field/Numeric.php | 217 ++++++++++++++++-- .../Component/Input/Field/Renderer.php | 24 ++ .../Component/Table/Renderer.php | 3 +- .../templates/default/Input/tpl.numeric.html | 2 +- 6 files changed, 274 insertions(+), 15 deletions(-) diff --git a/components/ILIAS/UI/resources/js/Input/Field/input.js b/components/ILIAS/UI/resources/js/Input/Field/input.js index 7484b08445ed..2e4ae15b4edf 100755 --- a/components/ILIAS/UI/resources/js/Input/Field/input.js +++ b/components/ILIAS/UI/resources/js/Input/Field/input.js @@ -18,6 +18,9 @@ il.UI.input = (function ($) { var onFieldUpdate = function (event, id, val) { var input = $("#" + id); var signals = signals_per_id[id]; + if (!signals || !signals.length) { + return; + } for (var i = 0; i < signals.length; i++) { var s = signals[i]; var options = s.options; diff --git a/components/ILIAS/UI/src/Component/Input/Field/Numeric.php b/components/ILIAS/UI/src/Component/Input/Field/Numeric.php index 67d97d47f974..3523e3badc4e 100755 --- a/components/ILIAS/UI/src/Component/Input/Field/Numeric.php +++ b/components/ILIAS/UI/src/Component/Input/Field/Numeric.php @@ -27,4 +27,44 @@ */ interface Numeric extends FilterInput { + /** + * Restrict input to integers only (no decimals, no scientific notation, no signs). + * This is a convenience method that sets all restriction flags. + */ + public function withIntegerOnly(): self; + + /** + * Prevent scientific notation (e, E). + */ + public function withPreventScientificNotation(bool $prevent = true): self; + + /** + * Prevent positive/negative signs (+ and -). + */ + public function withPreventSigns(bool $prevent = true): self; + + /** + * Prevent decimal separators (. and ,). + */ + public function withPreventDecimals(bool $prevent = true): self; + + /** + * Check if integer-only mode is enabled. + */ + public function isIntegerOnly(): bool; + + /** + * Check if scientific notation is prevented. + */ + public function isScientificNotationPrevented(): bool; + + /** + * Check if signs are prevented. + */ + public function areSignsPrevented(): bool; + + /** + * Check if decimals are prevented. + */ + public function areDecimalsPrevented(): bool; } diff --git a/components/ILIAS/UI/src/Implementation/Component/Input/Field/Numeric.php b/components/ILIAS/UI/src/Implementation/Component/Input/Field/Numeric.php index 6b3729dd8204..ec68ff46a697 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Input/Field/Numeric.php +++ b/components/ILIAS/UI/src/Implementation/Component/Input/Field/Numeric.php @@ -32,6 +32,11 @@ */ class Numeric extends FormInput implements C\Input\Field\Numeric { + private bool $integer_only = false; + private bool $prevent_scientific_notation = false; + private bool $prevent_signs = false; + private bool $prevent_decimals = false; + public function __construct( DataFactory $data_factory, \ILIAS\Refinery\Factory $refinery, @@ -60,9 +65,6 @@ protected function isClientSideValueOk($value): bool return is_numeric($value) || $value === "" || $value === null; } - /** - * @inheritdoc - */ protected function getConstraintForRequirement(): ?Constraint { if ($this->requirement_constraint !== null) { @@ -72,20 +74,209 @@ protected function getConstraintForRequirement(): ?Constraint return $this->refinery->numeric()->isNumeric(); } - /** - * @inheritdoc - */ + public function withIntegerOnly(): self + { + $clone = clone $this; + $clone->integer_only = true; + $clone->prevent_scientific_notation = true; + $clone->prevent_signs = true; + $clone->prevent_decimals = true; + + return $clone; + } + + public function withPreventScientificNotation(bool $prevent = true): self + { + $clone = clone $this; + $clone->prevent_scientific_notation = $prevent; + + return $clone; + } + + public function withPreventSigns(bool $prevent = true): self + { + $clone = clone $this; + $clone->prevent_signs = $prevent; + + return $clone; + } + + public function withPreventDecimals(bool $prevent = true): self + { + $clone = clone $this; + $clone->prevent_decimals = $prevent; + + return $clone; + } + + public function isIntegerOnly(): bool + { + return $this->integer_only; + } + + public function isScientificNotationPrevented(): bool + { + return $this->prevent_scientific_notation; + } + + public function areSignsPrevented(): bool + { + return $this->prevent_signs; + } + + public function areDecimalsPrevented(): bool + { + return $this->prevent_decimals; + } + public function getUpdateOnLoadCode(): Closure { - return fn($id) => "$('#$id').on('input', function(event) { - il.UI.input.onFieldUpdate(event, '$id', $('#$id').val()); - }); - il.UI.input.onFieldUpdate(event, '$id', $('#$id').val());"; + $prevent_e = $this->prevent_scientific_notation ? 'true' : 'false'; + $prevent_signs = $this->prevent_signs ? 'true' : 'false'; + $prevent_decimals = $this->prevent_decimals ? 'true' : 'false'; + $integer_only = $this->integer_only ? 'true' : 'false'; + + return static fn($id) => "(function() { + const input = document.getElementById('$id'); + if (!input) return; + + const preventE = $prevent_e; + const preventSigns = $prevent_signs; + const preventDecimals = $prevent_decimals; + const integerOnly = $integer_only; + + const blockedChars = []; + const blockedKeyCodes = []; + + if (integerOnly || preventE) { + blockedChars.push('e', 'E'); + } + if (integerOnly || preventSigns) { + blockedChars.push('+', '-'); + blockedKeyCodes.push(187, 189, 173, 107, 109); + } + if (integerOnly || preventDecimals) { + blockedChars.push('.', ','); + blockedKeyCodes.push(190, 188); + } + + // Define allowed keycodes + const allowedKeyCodes = [ + 8, // backspace + 9, // tab + 13, // enter + 27, // escape + 35, // end + 36, // home + 37, // left arrow + 38, // up arrow + 39, // right arrow + 46 // delete + ]; + + const ctrlKeyCodes = [65, 67, 86, 88]; // Ctrl+A, Ctrl+C, Ctrl+V, Ctrl+X + + const charsToRemove = []; + if (integerOnly || preventE) { + charsToRemove.push('e', 'E'); + } + if (integerOnly || preventSigns) { + charsToRemove.push('+', '-'); + } + if (integerOnly || preventDecimals) { + charsToRemove.push('.', ','); + } + + const escapeChar = function(c) { + var special = ['.', '*', '+', '?', '^', '$', '(', ')', '|', '[', ']', '\\\\']; + if (special.indexOf(c) !== -1) { + return '\\\\' + c; + } + return c; + }; + const escapedChars = charsToRemove.length > 0 ? charsToRemove.map(escapeChar).join('') : ''; + const cleanRegex = escapedChars ? new RegExp('[' + escapedChars + ']', 'g') : null; + + input.addEventListener('input', function(event) { + let value = input.value; + if (value === null || value === undefined) { + value = ''; + } + + if (cleanRegex) { + let cleaned = String(value).replace(cleanRegex, ''); + if (value !== cleaned) { + input.value = cleaned; + value = cleaned; + } + } + + il.UI.input.onFieldUpdate(event, '$id', value); + }); + + il.UI.input.onFieldUpdate(null, '$id', input.value); + + input.addEventListener('keydown', function(event) { + const key = event.key; + const keyCode = event.which || event.keyCode; + const isCtrl = event.ctrlKey || event.metaKey; + + if (blockedChars.includes(key)) { + event.preventDefault(); + return false; + } + + if (blockedKeyCodes.includes(keyCode)) { + event.preventDefault(); + return false; + } + + if (allowedKeyCodes.includes(keyCode)) { + return true; + } + + if (isCtrl && ctrlKeyCodes.includes(keyCode)) { + return true; + } + + if ((keyCode >= 48 && keyCode <= 57) || (keyCode >= 96 && keyCode <= 105)) { + return true; + } + + if (integerOnly) { + event.preventDefault(); + return false; + } + }); + + input.addEventListener('paste', function(event) { + const paste = (event.clipboardData || window.clipboardData).getData('text'); + let testPattern = '^[0-9'; + + if (integerOnly) { + testPattern = '^[0-9]*$'; + } else { + if (!preventSigns) { + testPattern += '+-'; + } + if (!preventDecimals) { + testPattern += '.,'; + } + if (!preventE) { + testPattern += 'eE'; + } + testPattern += ']*$'; + } + + const regex = new RegExp(testPattern); + if (!regex.test(paste)) { + event.preventDefault(); + return false; + } + }); + })();"; } - /** - * @inheritdoc - */ public function isComplex(): bool { return false; diff --git a/components/ILIAS/UI/src/Implementation/Component/Input/Field/Renderer.php b/components/ILIAS/UI/src/Implementation/Component/Input/Field/Renderer.php index 2e1f91b43515..b33c6389a050 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Input/Field/Renderer.php +++ b/components/ILIAS/UI/src/Implementation/Component/Input/Field/Renderer.php @@ -298,12 +298,36 @@ protected function renderTextField(F\Text $component): string protected function renderNumericField(F\Numeric $component, RendererInterface $default_renderer): string { + $component = $component->withAdditionalOnLoadCode($component->getUpdateOnLoadCode()); + $tpl = $this->getTemplate("tpl.numeric.html", true, true); $this->applyName($component, $tpl); $this->applyValue($component, $tpl, $this->escapeSpecialChars()); $label_id = $this->createId(); $tpl->setVariable('ID', $label_id); + + if ($component->isIntegerOnly()) { + $tpl->setCurrentBlock('data_integer_only'); + $tpl->setVariable('DATA_INTEGER_ONLY', 'data-integer-only="true"'); + $tpl->parseCurrentBlock(); + } + if ($component->isScientificNotationPrevented()) { + $tpl->setCurrentBlock('data_prevent_e'); + $tpl->setVariable('DATA_PREVENT_E', 'data-prevent-scientific="true"'); + $tpl->parseCurrentBlock(); + } + if ($component->areSignsPrevented()) { + $tpl->setCurrentBlock('data_prevent_signs'); + $tpl->setVariable('DATA_PREVENT_SIGNS', 'data-prevent-signs="true"'); + $tpl->parseCurrentBlock(); + } + if ($component->areDecimalsPrevented()) { + $tpl->setCurrentBlock('data_prevent_decimals'); + $tpl->setVariable('DATA_PREVENT_DECIMALS', 'data-prevent-decimals="true"'); + $tpl->parseCurrentBlock(); + } + return $this->wrapInFormContext($component, $component->getLabel(), $tpl->get(), $label_id); } diff --git a/components/ILIAS/UI/src/Implementation/Component/Table/Renderer.php b/components/ILIAS/UI/src/Implementation/Component/Table/Renderer.php index 3e7f02d6ec67..4c49add699b5 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Table/Renderer.php +++ b/components/ILIAS/UI/src/Implementation/Component/Table/Renderer.php @@ -578,7 +578,8 @@ public function getNewDedicatedName(string $dedicated_name): string $input = $this->getUIFactory()->input()->field()->numeric($numeric_label) ->withDedicatedName($component->getId()) ->withNameFrom($namesource) - ->withValue($component->getPosition() * 10); + ->withValue($component->getPosition() * 10) + ->withIntegerOnly(); $cell_tpl->setVariable('ORDER_INPUT', $default_renderer->render($input)); return $cell_tpl->get(); diff --git a/components/ILIAS/UI/src/templates/default/Input/tpl.numeric.html b/components/ILIAS/UI/src/templates/default/Input/tpl.numeric.html index c473e4d993d2..8e1cbcd51d40 100755 --- a/components/ILIAS/UI/src/templates/default/Input/tpl.numeric.html +++ b/components/ILIAS/UI/src/templates/default/Input/tpl.numeric.html @@ -1 +1 @@ - value="{VALUE}" name="{NAME}" {DISABLED} class="c-field-number" /> + value="{VALUE}" name="{NAME}" {DISABLED} {DATA_INTEGER_ONLY} {DATA_PREVENT_E} {DATA_PREVENT_SIGNS} {DATA_PREVENT_DECIMALS} class="c-field-number" /> From ea2c243931099abd07593b4527506b3ff6094244 Mon Sep 17 00:00:00 2001 From: Fabian Helfer Date: Thu, 8 Jan 2026 15:10:31 +0100 Subject: [PATCH 2/3] [Fix] #0045447 UI: Fix UnitTests --- .../Test/tests/Scoring/Settings/ScoreSettingsTest.php | 7 ++++--- .../Component/Input/Field/CommonFieldRendering.php | 3 ++- .../tests/Component/Input/Field/NumericInputTest.php | 10 ++++++++-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/components/ILIAS/Test/tests/Scoring/Settings/ScoreSettingsTest.php b/components/ILIAS/Test/tests/Scoring/Settings/ScoreSettingsTest.php index 5271489510c3..9754a5837082 100755 --- a/components/ILIAS/Test/tests/Scoring/Settings/ScoreSettingsTest.php +++ b/components/ILIAS/Test/tests/Scoring/Settings/ScoreSettingsTest.php @@ -468,7 +468,8 @@ public function testScoreSettingsSectionGamification(): void 'tst_highscore_top_num_description', 'id_3', null, - '' + '', + 'id_4' ); @@ -482,7 +483,7 @@ public function testScoreSettingsSectionGamification(): void ]; foreach ($opts as $index => $entry) { list($label, $byline) = $entry; - $nr = (string) ($index + 4); + $nr = (string) ($index + 5); $field_html = ''; $fields .= $this->getFormWrappedHtml( 'checkbox-field-input', @@ -512,7 +513,7 @@ public function testScoreSettingsSectionGamification(): void null, null, null, - '' + '', ); $this->assertHTMLEquals($expected, $this->brutallyTrimSignals($actual)); } diff --git a/components/ILIAS/UI/tests/Component/Input/Field/CommonFieldRendering.php b/components/ILIAS/UI/tests/Component/Input/Field/CommonFieldRendering.php index e1b1d603ef91..808ab9b437ca 100644 --- a/components/ILIAS/UI/tests/Component/Input/Field/CommonFieldRendering.php +++ b/components/ILIAS/UI/tests/Component/Input/Field/CommonFieldRendering.php @@ -120,6 +120,7 @@ protected function getFormWrappedHtml( ?string $label_id = null, ?string $js_id = null, ?string $name = 'name_0', + ?string $fieldset_id = null ): string { $label_id = $label_id ? " for=\"$label_id\"" : ''; $tab = $label_id ? '' : ' tabindex="0"'; @@ -133,7 +134,7 @@ protected function getFormWrappedHtml( } $html = ' -
+
' . $headline_tag_open . $label . $headline_tag_close . '
'; $html .= $payload_field; diff --git a/components/ILIAS/UI/tests/Component/Input/Field/NumericInputTest.php b/components/ILIAS/UI/tests/Component/Input/Field/NumericInputTest.php index fa5128264441..8f351f448665 100755 --- a/components/ILIAS/UI/tests/Component/Input/Field/NumericInputTest.php +++ b/components/ILIAS/UI/tests/Component/Input/Field/NumericInputTest.php @@ -63,7 +63,10 @@ public function testRender(): void $label, '', $byline, - 'id_1' + 'id_1', + null, + 'name_0', + 'id_2' ); $this->assertEquals($expected, $this->render($numeric)); } @@ -93,7 +96,10 @@ public function testRenderValue(): void $label, '', null, - 'id_1' + 'id_1', + null, + 'name_0', + 'id_2' ); $this->assertEquals($expected, $this->render($numeric)); } From b9cde52447d406b96092e2eb45ea1167f255833e Mon Sep 17 00:00:00 2001 From: Fabian Helfer Date: Thu, 8 Jan 2026 15:15:52 +0100 Subject: [PATCH 3/3] [Fix] #0045447 UI: Fix Copyright --- .../ILIAS/UI/resources/js/Input/Field/input.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/components/ILIAS/UI/resources/js/Input/Field/input.js b/components/ILIAS/UI/resources/js/Input/Field/input.js index 2e4ae15b4edf..06b483db06d0 100755 --- a/components/ILIAS/UI/resources/js/Input/Field/input.js +++ b/components/ILIAS/UI/resources/js/Input/Field/input.js @@ -1,3 +1,19 @@ +/** + * This file is part of ILIAS, a powerful learning management system + * published by ILIAS open source e-Learning e.V. + * + * ILIAS is licensed with the GPL-3.0, + * see https://www.gnu.org/licenses/gpl-3.0.en.html + * You should have received a copy of said license along with the + * source code, too. + * + * If this is not the case or you just want to try ILIAS, you'll find + * us at: + * https://www.ilias.de + * https://github.com/ILIAS-eLearning + * + *********************************************************************/ + /** * Input *