diff --git a/app/assets/javascripts/application_legacy.js b/app/assets/javascripts/application_legacy.js
index 18590f2a9..84138cd9b 100644
--- a/app/assets/javascripts/application_legacy.js
+++ b/app/assets/javascripts/application_legacy.js
@@ -18,7 +18,6 @@
//= require_self
//= require big
//= require units-converter
-//= require validation-utils
//= require migrate-units-form
//= require article-form
//= require unit-conversion-field
diff --git a/app/assets/javascripts/group-order-form.js b/app/assets/javascripts/group-order-form.js
index 81edda4d8..bbb9f2f2a 100644
--- a/app/assets/javascripts/group-order-form.js
+++ b/app/assets/javascripts/group-order-form.js
@@ -11,47 +11,14 @@ class GroupOrderForm {
this.groupBalance = config.groupBalance;
this.minimumBalance = config.minimumBalance;
- ValidationUtils.setupCustomValidation(this.form$);
-
this.initializeIncreaseDecreaseButtons();
this.submitButton$.removeAttr('disabled');
- this.initializeFormValidation();
}
initializeIncreaseDecreaseButtons() {
this.articleRows$.each((_, element) => this.initializeOrderArticleRow($(element)));
}
- initializeFormValidation() {
- this.form$.on('submit', (e) => {
- // Update all validation messages first
- this.articleRows$.each((_, row) => {
- const row$ = $(row);
- this.updateValidationMessages(row$);
- });
-
- // Check if form is valid - browser will prevent submission if invalid
- if (!this.form$[0].checkValidity()) {
- e.preventDefault();
-
- // Show error messages for all invalid inputs
- this.form$.find(':invalid').each((_, invalidInput) => {
- const input$ = $(invalidInput);
- if (input$.hasClass('goa-quantity')) {
- // Trigger validation to show error message
- ValidationUtils.validateNumericInput(input$);
- }
- });
-
- // Focus on first invalid input
- const firstInvalid = this.form$.find(':invalid').first();
- if (firstInvalid.length > 0) {
- firstInvalid.focus();
- }
- }
- });
- }
-
initializeOrderArticleRow(row$) {
const quantity$ = row$.find('.goa-quantity');
const tolerance$ = row$.find('.goa-tolerance');
@@ -67,20 +34,29 @@ class GroupOrderForm {
row$.find('.btn-ordering.increase').click((event) => this.increaseOrDecrease($(event.target).parents('.btn-group').find('input.numeric'), true));
quantityAndTolerance$.change(() => {
+ this.updateValidationErrors(quantity$);
this.updateMissingUnits(row$, quantity$);
this.updateBalance();
- this.updateValidationMessages(row$);
});
quantityAndTolerance$.keyup(() => quantity$.trigger('change'));
+ }
- // Handle validation events for quantity field only
- quantity$.on('invalid', (e) => {
- e.preventDefault();
- e.stopPropagation();
- this.updateValidationMessages(row$);
- });
+ updateValidationErrors(valueField$) {
+ const errorContainer$ = valueField$
+ // TODO: This parent().parent() is quite arbitrary:
+ .parent().parent();
+ errorContainer$.find('.quantity-field-invalid').remove();
- this.updateValidationMessages(row$);
+ if (valueField$.is(':invalid')) {
+ if (valueField$[0].validity.stepMismatch || valueField$[0].validity.rangeUnderflow) {
+ const errorSpan$ = $(`
${I18n.t('errors.step_error', {min: 0, granularity: valueField$.attr('step')})}
`);
+ errorContainer$.append(errorSpan$);
+ }
+ if (valueField$[0].validity.rangeOverflow) {
+ const errorSpan$ = $(`${I18n.t('errors.maximum_quantity_error', {max: valueField$.attr('max')})}
`);
+ errorContainer$.append(errorSpan$);
+ }
+ }
}
updateBalance() {
@@ -102,8 +78,8 @@ class GroupOrderForm {
// determine bgcolor and submit button state according to balance
var bgcolor = '';
if (balance < this.minimumBalance) {
- bgcolor = '#FF0000';
- this.submitButton$.attr('disabled', 'disabled')
+ bgcolor = '#FF0000';
+ this.submitButton$.attr('disabled', 'disabled')
} else {
this.submitButton$.removeAttr('disabled')
}
@@ -269,30 +245,5 @@ class GroupOrderForm {
var remainder = Big(quantity).mod(packSize).toNumber();
return (remainder > 0 && (Big(remainder).add(tolerance).toNumber() >= packSize));
}
-
- updateValidationMessages(row$) {
- const quantity$ = row$.find('.goa-quantity');
-
- if (quantity$.length === 0) return;
-
- const inputElement = quantity$[0];
-
- // Clear any existing validation state
- inputElement.setCustomValidity('');
-
- // Use ValidationUtils for validation with error spans
- ValidationUtils.validateNumericInput(quantity$);
-
- // Special case: item not available (max = 0)
- const rawValue = quantity$.val().trim();
- const inputValue = parseFloat(rawValue);
- const maxValue = parseFloat(quantity$.attr('max'));
-
- if (maxValue === 0 && inputValue > 0) {
- const message = I18n.t('errors.item_not_available');
- inputElement.setCustomValidity(message);
- ValidationUtils.showValidationMessage(quantity$, message);
- }
- }
}
diff --git a/app/assets/javascripts/unit-conversion-field.js b/app/assets/javascripts/unit-conversion-field.js
index a5773f076..08226d7d5 100644
--- a/app/assets/javascripts/unit-conversion-field.js
+++ b/app/assets/javascripts/unit-conversion-field.js
@@ -185,22 +185,14 @@
const unitLabel = this.unitSelectOptions.find(option => option.value === unit).label;
this.conversionResult$.text('= ' + this.getConversionResult() + ' x ' + unitLabel);
this.conversionResult$.parent().find('.numeric-step-error').remove();
+ if (this.quantityInput$.is(':invalid')) {
+ this.applyButton$.attr('disabled', 'disabled');
- const errorSpan$ = $('');
-
- // Use ValidationUtils for proper custom error messages
- const isValid = ValidationUtils.validateNumericInput(this.quantityInput$, {
- errorSpan$: errorSpan$
- });
-
- if (isValid) {
- this.applyButton$.removeAttr('disabled');
+ const errorSpan$ = $(`${I18n.t('errors.step_error', {min: 0, granularity: this.quantityInput$.attr('step')})}
`);
+ errorSpan$.show();
+ this.conversionResult$.after(errorSpan$);
} else {
- this.applyButton$.attr('disabled', 'disabled');
- if (errorSpan$.text()) {
- errorSpan$.show();
- this.conversionResult$.after(errorSpan$);
- }
+ this.applyButton$.removeAttr('disabled');
}
}
diff --git a/app/assets/javascripts/validation-utils.js b/app/assets/javascripts/validation-utils.js
deleted file mode 100644
index 59cadcd08..000000000
--- a/app/assets/javascripts/validation-utils.js
+++ /dev/null
@@ -1,155 +0,0 @@
-// Shared validation utilities for form inputs
-class ValidationUtils {
- /**
- * Sets up custom validation messages while keeping browser validation active
- * @param {jQuery} form$ - The form element
- */
- static setupCustomValidation(form$) {
- if (form$.length === 0) return;
-
- // Keep HTML5 validation active but override messages
- // Don't set novalidate - we want the browser to prevent submission
-
- // Set up custom validation for quantity inputs only
- form$.find('input[type="number"].goa-quantity').each((_, input) => {
- const input$ = $(input);
-
- // Set up event handlers for custom validation messages
- input$.on('invalid.customValidation', (e) => {
- e.preventDefault(); // Prevent browser validation popup
-
- // Apply our custom validation and show message in popover
- ValidationUtils.validateNumericInput(input$, {
- showInSpanOnly: true // Don't use setCustomValidity for display
- });
- });
- });
- }
- /**
- * Validates a numeric input and sets custom validation messages
- * @param {jQuery} input$ - The input element to validate
- * @param {Object} options - Validation options
- * @returns {boolean} - Whether the input is valid
- */
- static validateNumericInput(input$, options = {}) {
- if (input$.length === 0) return true;
-
- const inputElement = input$[0];
- const rawValue = input$.val().trim().replace(',', '.');
- const inputValue = parseFloat(rawValue);
-
- let customMessage = '';
-
- // Only validate if we have a valid number
- if (rawValue !== '' && !isNaN(inputValue)) {
- // Quantity field validation only
- customMessage = ValidationUtils.validateQuantityField(input$, inputValue);
- }
-
- // Show validation message in error span (avoid popover conflicts)
- ValidationUtils.showValidationMessage(input$, customMessage);
-
- // Update visual error span if it exists (keep for backward compatibility)
- if (options.errorSpan$ && options.errorSpan$.length > 0) {
- if (customMessage) {
- options.errorSpan$.text(customMessage).show();
- } else {
- options.errorSpan$.hide();
- }
- }
-
- // Set custom validation message for browser validation
- // This will be used by the browser to prevent form submission
- if (!options.showInSpanOnly) {
- inputElement.setCustomValidity(customMessage);
- }
-
- // Return validation state
- return customMessage === '';
- }
-
- /**
- * Validates a quantity field
- * @param {jQuery} input$ - The quantity input element
- * @param {number} inputValue - The parsed input value
- * @returns {string} - Error message or empty string if valid
- */
- static validateQuantityField(input$, inputValue) {
- // Get validation constraints from HTML attributes
- const maxValue = input$.attr('max') ? parseFloat(input$.attr('max')) : null;
- const minValue = parseFloat(input$.attr('min')) || 0;
- const stepValue = parseFloat(input$.attr('step')) || 1;
-
- // Check maximum quantity first (highest priority)
- if (maxValue !== null && inputValue > maxValue) {
- return I18n.t('errors.maximum_quantity_error', { max: maxValue });
- }
- // Check minimum value
- else if (inputValue < minValue) {
- return I18n.t('errors.step_error', { min: minValue, granularity: stepValue });
- }
- // Check step/granularity (allow for floating point precision issues)
- else if (stepValue > 0) {
- const remainder = ((inputValue - minValue) % stepValue);
- if (Math.abs(remainder) > 0.0001 && Math.abs(remainder - stepValue) > 0.0001) {
- return I18n.t('errors.step_error', { min: minValue, granularity: stepValue });
- }
- }
-
- return '';
- }
-
- /**
- * Shows or hides validation message using error spans (avoids popover conflicts)
- * @param {jQuery} input$ - The input element
- * @param {string} message - The validation message (empty to hide)
- */
- static showValidationMessage(input$, message) {
- if (input$.length === 0) return;
-
- // Find the error span for this input
- const errorSpan$ = input$.closest('.group-order-input').find('.numeric-step-error');
-
- if (message) {
- // Show error message in span and add error styling
- if (errorSpan$.length > 0) {
- errorSpan$.text(message).show();
- }
- input$.addClass('validation-error');
- } else {
- // Hide error message and remove styling
- if (errorSpan$.length > 0) {
- errorSpan$.hide();
- }
- input$.removeClass('validation-error');
- }
- }
-
- /**
- * Hides all validation messages
- */
- static hideAllValidationMessages() {
- $('.validation-error').removeClass('validation-error');
- $('.numeric-step-error').hide();
- }
-
-
-
- /**
- * Sets up validation for a form input
- * @param {jQuery} input$ - The input element
- * @param {Object} options - Validation options
- */
- static setupInputValidation(input$, options = {}) {
- if (input$.length === 0) return;
-
- // Only set up event handlers, don't trigger validation immediately
- // to avoid recursion when called from existing event handlers
- input$.on('invalid.validationUtils', (e) => {
- e.preventDefault();
- e.stopPropagation();
- ValidationUtils.validateNumericInput(input$, options);
- return false;
- });
- }
-}
\ No newline at end of file
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index 5b7785d3d..dbd57ebbf 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -11,4 +11,3 @@
@import "list.missing";
@import "recurring_select";
@import "actiontext";
-@import "validation";
diff --git a/app/assets/stylesheets/bootstrap_and_overrides.css.scss b/app/assets/stylesheets/bootstrap_and_overrides.css.scss
index cee179f3e..299f2076b 100644
--- a/app/assets/stylesheets/bootstrap_and_overrides.css.scss
+++ b/app/assets/stylesheets/bootstrap_and_overrides.css.scss
@@ -314,7 +314,7 @@ tr.order-article {
}
.units_received_cell {
-
+
.btn-group .btn-ordering {
margin-top: 0;
@@ -704,8 +704,7 @@ td.mr-1 > *:last-child {
user-select: none;
}
-.numeric-step-error {
- display: none;
+.quantity-field-invalid {
color: #b94a48;
}
@@ -713,11 +712,6 @@ td.mr-1 > *:last-child {
border: 1px solid #b94a48;
}
-.btn-group.numeric-step:has(> input:invalid) + .numeric-step-error {
- display: block;
-}
-
-
.form-group .select2-container {
width: 200px;
}
diff --git a/app/assets/stylesheets/validation.scss b/app/assets/stylesheets/validation.scss
deleted file mode 100644
index b44d35826..000000000
--- a/app/assets/stylesheets/validation.scss
+++ /dev/null
@@ -1,19 +0,0 @@
-// Simple validation error styling for inputs
-input.validation-error {
- border-color: #d9534f !important;
- box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 6px #ce8483 !important;
-
- &:focus {
- border-color: #d9534f !important;
- box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 6px #ce8483 !important;
- }
-}
-
-// Style error message spans
-.numeric-step-error {
- color: #d9534f;
- font-size: 12px;
- font-weight: 500;
- margin-top: 2px;
- display: block;
-}
\ No newline at end of file
diff --git a/app/views/group_orders/_form.html.haml b/app/views/group_orders/_form.html.haml
index 698f19d6d..f1fd4c43d 100644
--- a/app/views/group_orders/_form.html.haml
+++ b/app/views/group_orders/_form.html.haml
@@ -116,7 +116,6 @@
- quantity_data['used_quantity'] = @ordering_data[:order_articles][order_article.id][:used_quantity]
- quantity_data['price'] = @ordering_data[:order_articles][order_article.id][:price]
- quantity_data['minimum_order_quantity'] = @ordering_data[:order_articles][order_article.id][:minimum_order_quantity] unless @ordering_data[:order_articles][order_article.id][:minimum_order_quantity].nil?
- - quantity_data['maximum_order_quantity'] = @ordering_data[:order_articles][order_article.id][:maximum_order_quantity] unless @ordering_data[:order_articles][order_article.id][:maximum_order_quantity].nil?
- quantity_data['e2e-order-article-id'] = order_article.id
%td.used-unused
%span.used= number_with_precision(@ordering_data[:order_articles][order_article.id][:used_quantity], precision: 3, strip_insignificant_zeros: true)
@@ -129,8 +128,6 @@
%a.btn.btn-ordering.increase
%i.glyphicon.glyphicon-plus
%input.goa-quantity{type: "number", name: "group_order[group_order_articles_attributes][#{order_article.id}][quantity]", value: @ordering_data[:order_articles][order_article.id][:quantity], data: quantity_data, autocomplete: 'off', class: 'form-control numeric', style: ('display:none' if @order.stockit?), min: 0, max: (@ordering_data[:order_articles][order_article.id][:quantity_available]), step: order_article.article_version.group_order_granularity}
- %span.numeric-step-error{style: 'display: none;'}
- = t('errors.step_error', granularity: order_article.article_version.group_order_granularity, min: 0)
%td.used-unused-tolerance{style: ('display:none' if @order.stockit?)}
- if (order_article.article_version.uses_tolerance?)