From e87e846aac91b755d02c91ecc0acd2ac498d1c8c Mon Sep 17 00:00:00 2001 From: Florian Motteau Date: Tue, 7 Oct 2025 16:27:20 +0200 Subject: [PATCH 1/2] Improved: support step attribute on number inputs OFBIZ-13297 --- framework/widget/dtd/widget-form.xsd | 5 +++++ .../ofbiz/widget/model/ModelFormField.java | 14 +++++++++++++ .../RenderableFtlFormElementsBuilder.java | 8 +++++++ .../RenderableFtlFormElementsBuilderTest.java | 21 +++++++++++++++++++ .../template/macro/HtmlFormMacroLibrary.ftl | 3 ++- 5 files changed, 50 insertions(+), 1 deletion(-) diff --git a/framework/widget/dtd/widget-form.xsd b/framework/widget/dtd/widget-form.xsd index 2be59b4fc7..3e9ee92b92 100644 --- a/framework/widget/dtd/widget-form.xsd +++ b/framework/widget/dtd/widget-form.xsd @@ -1560,6 +1560,11 @@ under the License. + + + Controls the "step" attribute of the input element. Ignored if the "type" attribute of the field is not "number". See https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/step + + Controls the pattern attribute of the input element. See https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/pattern diff --git a/framework/widget/src/main/java/org/apache/ofbiz/widget/model/ModelFormField.java b/framework/widget/src/main/java/org/apache/ofbiz/widget/model/ModelFormField.java index 830ac1444e..be7f2a577f 100644 --- a/framework/widget/src/main/java/org/apache/ofbiz/widget/model/ModelFormField.java +++ b/framework/widget/src/main/java/org/apache/ofbiz/widget/model/ModelFormField.java @@ -5918,6 +5918,7 @@ public static class TextField extends FieldInfo { private final SubHyperlink subHyperlink; private final String type; private final String pattern; + private final String step; public TextField(Element element, ModelFormField modelFormField) { super(element, modelFormField); @@ -5925,6 +5926,7 @@ public TextField(Element element, ModelFormField modelFormField) { this.defaultValue = FlexibleStringExpander.getInstance(element.getAttribute("default-value")); this.mask = element.getAttribute("mask"); this.type = element.getAttribute("type"); + this.step = element.getAttribute("step"); this.pattern = element.getAttribute("pattern"); Integer maxlength = null; String maxlengthStr = element.getAttribute("maxlength"); @@ -5964,6 +5966,7 @@ protected TextField(int fieldSource, int size, Integer maxlength, ModelFormField this.defaultValue = FlexibleStringExpander.getInstance(""); this.mask = ""; this.type = ""; + this.step = ""; this.pattern = ""; this.maxlength = maxlength; this.placeholder = FlexibleStringExpander.getInstance(""); @@ -5978,6 +5981,7 @@ protected TextField(int fieldSource, int size, Integer maxlength, String type, M this.defaultValue = FlexibleStringExpander.getInstance(""); this.mask = ""; this.type = type; + this.step = ""; this.pattern = ""; this.maxlength = maxlength; this.placeholder = FlexibleStringExpander.getInstance(""); @@ -5992,6 +5996,7 @@ private TextField(int fieldSource, int fieldType, ModelFormField modelFormField) this.defaultValue = FlexibleStringExpander.getInstance(""); this.mask = ""; this.type = ""; + this.step = ""; this.pattern = ""; this.maxlength = null; this.placeholder = FlexibleStringExpander.getInstance(""); @@ -6014,6 +6019,7 @@ protected TextField(TextField original, ModelFormField modelFormField) { this.defaultValue = original.defaultValue; this.mask = original.mask; this.type = original.type; + this.step = original.step; this.pattern = original.pattern; this.placeholder = original.placeholder; this.size = original.size; @@ -6135,6 +6141,14 @@ public String getType() { return this.type; } + /** + * Gets step. + * @return the step + */ + public String getStep() { + return this.step; + } + /** * Gets pattern. * @return the pattern diff --git a/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/RenderableFtlFormElementsBuilder.java b/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/RenderableFtlFormElementsBuilder.java index 2c91b3032b..e53f28e348 100644 --- a/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/RenderableFtlFormElementsBuilder.java +++ b/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/RenderableFtlFormElementsBuilder.java @@ -255,6 +255,13 @@ public RenderableFtl textField(final Map context, final ModelFor if (UtilValidate.isEmpty(type)) { type = "text"; } + String step = ""; + if (type.equals("number")) { + step = textField.getStep(); + if (UtilValidate.isEmpty(step)) { + step = "any"; + } + } String pattern = ""; if (List.of("text", "email", "url", "tel").contains(type)) { pattern = textField.getPattern(); @@ -308,6 +315,7 @@ public RenderableFtl textField(final Map context, final ModelFor .stringParameter("name", name) .stringParameter("className", String.join(" ", classes)) .stringParameter("type", type) + .stringParameter("step", step) .stringParameter("pattern", pattern) .stringParameter("alert", alert) .stringParameter("value", value) diff --git a/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/RenderableFtlFormElementsBuilderTest.java b/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/RenderableFtlFormElementsBuilderTest.java index 13a15f5ee6..37144fe702 100644 --- a/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/RenderableFtlFormElementsBuilderTest.java +++ b/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/RenderableFtlFormElementsBuilderTest.java @@ -252,6 +252,7 @@ public void textFieldDefaultTypeIsText(@Mocked final ModelFormField.TextField te final RenderableFtl renderableFtl = renderableFtlFormElementsBuilder.textField(Map.of("session", httpSession), textField, true); assertThat(renderableFtl, MacroCallMatcher.hasName("renderTextField")); assertThat(renderableFtl, MacroCallMatcher.hasParameters(MacroCallParameterMatcher.hasNameAndStringValue("type", "text"))); + assertThat(renderableFtl, MacroCallMatcher.hasParameters(MacroCallParameterMatcher.hasNameAndStringValue("step", ""))); } @Test @@ -266,6 +267,23 @@ public void textFieldTypeNumber(@Mocked final ModelFormField.TextField textField final RenderableFtl renderableFtl = renderableFtlFormElementsBuilder.textField(Map.of("session", httpSession), textField, true); assertThat(renderableFtl, MacroCallMatcher.hasName("renderTextField")); assertThat(renderableFtl, MacroCallMatcher.hasParameters(MacroCallParameterMatcher.hasNameAndStringValue("type", "number"))); + assertThat(renderableFtl, MacroCallMatcher.hasParameters(MacroCallParameterMatcher.hasNameAndStringValue("step", "any"))); + } + + @Test + public void textFieldTypeNumberCustomStep(@Mocked final ModelFormField.TextField textField) { + new Expectations() { + { + textField.getType(); result = "number"; + textField.getStep(); result = "0.1"; + httpSession.getAttribute("delegatorName"); result = "DelegatorName"; + } + }; + + final RenderableFtl renderableFtl = renderableFtlFormElementsBuilder.textField(Map.of("session", httpSession), textField, true); + assertThat(renderableFtl, MacroCallMatcher.hasName("renderTextField")); + assertThat(renderableFtl, MacroCallMatcher.hasParameters(MacroCallParameterMatcher.hasNameAndStringValue("type", "number"))); + assertThat(renderableFtl, MacroCallMatcher.hasParameters(MacroCallParameterMatcher.hasNameAndStringValue("step", "0.1"))); } @Test @@ -280,6 +298,7 @@ public void textFieldTypeEmail(@Mocked final ModelFormField.TextField textField) final RenderableFtl renderableFtl = renderableFtlFormElementsBuilder.textField(Map.of("session", httpSession), textField, true); assertThat(renderableFtl, MacroCallMatcher.hasName("renderTextField")); assertThat(renderableFtl, MacroCallMatcher.hasParameters(MacroCallParameterMatcher.hasNameAndStringValue("type", "email"))); + assertThat(renderableFtl, MacroCallMatcher.hasParameters(MacroCallParameterMatcher.hasNameAndStringValue("step", ""))); } @Test @@ -294,6 +313,7 @@ public void textFieldTypeUrl(@Mocked final ModelFormField.TextField textField) { final RenderableFtl renderableFtl = renderableFtlFormElementsBuilder.textField(Map.of("session", httpSession), textField, true); assertThat(renderableFtl, MacroCallMatcher.hasName("renderTextField")); assertThat(renderableFtl, MacroCallMatcher.hasParameters(MacroCallParameterMatcher.hasNameAndStringValue("type", "url"))); + assertThat(renderableFtl, MacroCallMatcher.hasParameters(MacroCallParameterMatcher.hasNameAndStringValue("step", ""))); } @Test @@ -308,6 +328,7 @@ public void textFieldTypeTel(@Mocked final ModelFormField.TextField textField) { final RenderableFtl renderableFtl = renderableFtlFormElementsBuilder.textField(Map.of("session", httpSession), textField, true); assertThat(renderableFtl, MacroCallMatcher.hasName("renderTextField")); assertThat(renderableFtl, MacroCallMatcher.hasParameters(MacroCallParameterMatcher.hasNameAndStringValue("type", "tel"))); + assertThat(renderableFtl, MacroCallMatcher.hasParameters(MacroCallParameterMatcher.hasNameAndStringValue("step", ""))); } @Test diff --git a/themes/common-theme/template/macro/HtmlFormMacroLibrary.ftl b/themes/common-theme/template/macro/HtmlFormMacroLibrary.ftl index a9db6463b5..19fce9d59f 100644 --- a/themes/common-theme/template/macro/HtmlFormMacroLibrary.ftl +++ b/themes/common-theme/template/macro/HtmlFormMacroLibrary.ftl @@ -43,7 +43,7 @@ under the License. <#macro renderHyperlinkField> -<#macro renderTextField type pattern name className alert value="" textSize="" maxlength="" id="" event="" action="" +<#macro renderTextField type step pattern name className alert value="" textSize="" maxlength="" id="" event="" action="" disabled=false clientAutocomplete="" ajaxUrl="" ajaxEnabled="" mask="" tabindex="" readonly="" required=false placeholder="" delegatorName="default"> @@ -69,6 +69,7 @@ under the License. <#if tabindex?has_content> tabindex="${tabindex}"<#rt/> <#if required?has_content && required> required <#if pattern?has_content> pattern="${pattern}" + <#if step?has_content> step="${step}" /><#t/> From a47e48205bf87b610e17f75ba68a969523593bf9 Mon Sep 17 00:00:00 2001 From: Florian Motteau Date: Wed, 8 Oct 2025 10:15:51 +0200 Subject: [PATCH 2/2] Fixed: always use en locale when formatting/parsing numeric values This is a quick fix, not supposed to make its way to a release without further discussions! OFBIZ-13297 --- .../java/org/apache/ofbiz/base/conversion/NumberConverters.java | 2 +- .../main/java/org/apache/ofbiz/widget/model/ModelFormField.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/framework/base/src/main/java/org/apache/ofbiz/base/conversion/NumberConverters.java b/framework/base/src/main/java/org/apache/ofbiz/base/conversion/NumberConverters.java index ad6a5032d3..0a44683d23 100644 --- a/framework/base/src/main/java/org/apache/ofbiz/base/conversion/NumberConverters.java +++ b/framework/base/src/main/java/org/apache/ofbiz/base/conversion/NumberConverters.java @@ -32,7 +32,7 @@ public class NumberConverters implements ConverterLoader { private static Number fromString(String str, Locale locale) throws ConversionException { - NumberFormat nf = NumberFormat.getNumberInstance(locale); + NumberFormat nf = NumberFormat.getNumberInstance(Locale.getDefault()); if (nf instanceof DecimalFormat) { // CHECKSTYLE_OFF: ALMOST_ALL ((DecimalFormat) nf).setParseBigDecimal(true); diff --git a/framework/widget/src/main/java/org/apache/ofbiz/widget/model/ModelFormField.java b/framework/widget/src/main/java/org/apache/ofbiz/widget/model/ModelFormField.java index be7f2a577f..f2f9cb947d 100644 --- a/framework/widget/src/main/java/org/apache/ofbiz/widget/model/ModelFormField.java +++ b/framework/widget/src/main/java/org/apache/ofbiz/widget/model/ModelFormField.java @@ -366,7 +366,7 @@ public String getEntry(Map context, String defaultValu if (retVal != null) { // format string based on the user's locale and time zone if (retVal instanceof Double || retVal instanceof Float || retVal instanceof BigDecimal) { - NumberFormat nf = NumberFormat.getInstance(locale); + NumberFormat nf = NumberFormat.getInstance(Locale.getDefault()); nf.setMaximumFractionDigits(10); return nf.format(retVal); } else if (retVal instanceof java.sql.Date) {