Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -468,7 +468,8 @@ public function testScoreSettingsSectionGamification(): void
'tst_highscore_top_num_description',
'id_3',
null,
''
'',
'id_4'
);


Expand All @@ -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 = '<input type="checkbox" id="id_' . $nr . '" value="checked" checked="checked" class="c-field-checkbox" />';
$fields .= $this->getFormWrappedHtml(
'checkbox-field-input',
Expand Down Expand Up @@ -512,7 +513,7 @@ public function testScoreSettingsSectionGamification(): void
null,
null,
null,
''
'',
);
$this->assertHTMLEquals($expected, $this->brutallyTrimSignals($actual));
}
Expand Down
19 changes: 19 additions & 0 deletions components/ILIAS/UI/resources/js/Input/Field/input.js
Original file line number Diff line number Diff line change
@@ -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
*
Expand All @@ -18,6 +34,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;
Expand Down
40 changes: 40 additions & 0 deletions components/ILIAS/UI/src/Component/Input/Field/Numeric.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
217 changes: 204 additions & 13 deletions components/ILIAS/UI/src/Implementation/Component/Input/Field/Numeric.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<input id="{ID}" type="number"<!-- BEGIN value --> value="{VALUE}"<!-- END value --><!-- BEGIN name --> name="{NAME}"<!-- END name --><!-- BEGIN disabled --> {DISABLED}<!-- END disabled --> class="c-field-number" />
<input id="{ID}" type="number"<!-- BEGIN value --> value="{VALUE}"<!-- END value --><!-- BEGIN name --> name="{NAME}"<!-- END name --><!-- BEGIN disabled --> {DISABLED}<!-- END disabled --><!-- BEGIN data_integer_only --> {DATA_INTEGER_ONLY}<!-- END data_integer_only --><!-- BEGIN data_prevent_e --> {DATA_PREVENT_E}<!-- END data_prevent_e --><!-- BEGIN data_prevent_signs --> {DATA_PREVENT_SIGNS}<!-- END data_prevent_signs --><!-- BEGIN data_prevent_decimals --> {DATA_PREVENT_DECIMALS}<!-- END data_prevent_decimals --> class="c-field-number" />
Loading