Allow formating numbers when rendering attributes

Closes keycloak#26320

Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
Pedro Igor 2024-01-21 19:24:34 -03:00
parent 93a6d56af3
commit 3a7ce54266
9 changed files with 135 additions and 2 deletions

View file

@ -536,6 +536,16 @@ Useful for numeric fields.
|inputTypeStep
|HTML input `step` attribute applied to the field - Specifies the interval between legal numbers in an input field. Useful for numeric fields.
|Number Format
|If set, the `data-kcNumberFormat` attribute is added to the field to format the value based on a given format. This annotation is targeted for numbers where the format is based on the
number of digits expected in a determined position. For instance, a format `(\{2}) \{5}-\{4}` will format the field value to `(00) 00000-0000`.
|Number UnFormat
|If set, the `data-kcNumberUnFormat` attribute is added to the field to format the value based on a given format before submitting the form. This annotation
is useful if you do not want to store any format for a specific attribute but only format the value on the client side. For instance, if the current value
is `(00) 00000-0000`, the value will change to `00000000000` if you set the value `\{11}` to this annotation or any other format you want by specifying a set of one or ore group of digits.
Make sure to add validators to perform server-side validations before storing values.
|===
[NOTE]
@ -710,6 +720,15 @@ provided by built-in `options` validation.
.Options provided by custom validator
image:images/user-profile-select-options-custom-validator.png[]
[[_adding-custom-html5-data-attributes]]
==== Adding Custom HTML5 Data Attributes
You can enable additional client-side behavior by using `kc*` annotations. These annotations are going to be added
automatically to a field as a HTML5 attribute prefixed with `data-` and a script with the same will be loaded to the dynamic pages.
For instance, if you add a `kcMyCustomValidation` annotation to a field, the dynamic pages will add a `data-kcMyCustomValidation` HTML5 attribute
to the field and load a JS script file from `<THEME TYPE>/resources/js/kcMyCustomValidation.js`. See the {developerguide_link}[{developerguide_name}] for more information about
how to deploy a custom JS script file to your theme.
== Forcing User Profile compliance

View file

@ -28,3 +28,9 @@ $evaluation.grant()
{$v
location.origin
keycloak.token
11
1
2
3
4
5

View file

@ -3045,3 +3045,5 @@ searchClientAuthorizationResource=Search resource
searchClientAuthorizationPolicy=Search policy
searchClientAuthorizationPermission=Search permission
userNotSaved=The user has not been saved\: {{error}}
kcNumberFormat=Number Format
kcNumberUnFormat=Number UnFormat

View file

@ -77,6 +77,14 @@ export const AttributeAnnotations = () => {
key: "inputTypeStep",
label: t("inputTypeStep"),
},
{
key: "kcNumberFormat",
label: t("kcNumberFormat"),
},
{
key: "kcNumberUnFormat",
label: t("kcNumberUnFormat"),
},
]}
/>
</GridItem>

View file

@ -4,6 +4,7 @@ import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -78,6 +79,13 @@ public abstract class AbstractUserProfileBean {
return attributes;
}
public Map<String, Object> getHtml5DataAnnotations() {
return getAttributes().stream().map(Attribute::getHtml5DataAnnotations)
.map(Map::entrySet)
.flatMap(Set::stream)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (l, r) -> l));
}
/**
* Get map of all attributes where attribute name is key. Useful to render crafted form.
*
@ -168,6 +176,11 @@ public abstract class AbstractUserProfileBean {
return annotations;
}
public Map<String, Object> getHtml5DataAnnotations() {
return getAnnotations().entrySet().stream()
.filter((entry) -> entry.getKey().startsWith("kc")).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
/**
* Get info about validators applied to attribute.
*

View file

@ -0,0 +1,48 @@
export const formatNumber = (input, format) => {
if (!input) {
return "";
}
// array holding the patterns for the number of expected digits in each part
const digitPattern = format.match(/{\d+}/g);
if (!digitPattern) {
return "";
}
// calculate the maximum size of the given pattern based on the sum of the expected digits
const maxSize = digitPattern.reduce((total, p) => total + parseInt(p.replace("{", "").replace("}", "")), 0)
// keep only digits
let rawValue = input.replace(/\D+/g, '');
// make sure the value is a number
if (parseInt(rawValue) != rawValue) {
return "";
}
// make sure the number of digits does not exceed the maximum size
if (rawValue.length > maxSize) {
rawValue = rawValue.substring(0, maxSize);
}
// build the regex based based on the expected digits in each part
const formatter = digitPattern.reduce((result, p) => result + `(\\d${p})`, "^");
// if the current digits match the pattern we have each group of digits in an array
let digits = new RegExp(formatter).exec(rawValue);
// no match, return the raw value without any format
if (!digits) {
return input;
}
let result = format;
// finally format the current digits accordingly to the given format
for (let i = 0; i < digitPattern.length; i++) {
result = result.replace(digitPattern[i], digits[i + 1]);
}
return result;
}

View file

@ -0,0 +1,14 @@
import {formatNumber} from "./common.js";
const DATA_KC_NUMBER_FORMAT = 'data-kcNumberFormat';
document.querySelectorAll(`[${DATA_KC_NUMBER_FORMAT}]`)
.forEach(input => {
const format = input.getAttribute(DATA_KC_NUMBER_FORMAT);
input.addEventListener('keyup', (event) => {
input.value = formatNumber(input.value, format);
});
input.value = formatNumber(input.value, format);
});

View file

@ -0,0 +1,15 @@
import {formatNumber} from "./common.js";
const DATA_KC_NUMBER_UNFORMAT = 'data-kcNumberUnFormat';
document.querySelectorAll(`[${DATA_KC_NUMBER_UNFORMAT}]`)
.forEach(input => {
for (let form of document.forms) {
form.addEventListener('submit', (event) => {
const rawFormat = input.getAttribute(DATA_KC_NUMBER_UNFORMAT);
if (rawFormat) {
input.value = formatNumber(input.value, rawFormat);
}
});
}
});

View file

@ -53,6 +53,10 @@
</div>
<#nested "afterField" attribute>
</#list>
<#list profile.html5DataAnnotations?keys as key>
<script type="module" src="${url.resourcesPath}/js/${key}.js"></script>
</#list>
</#macro>
<#macro inputFieldByType attribute>
@ -86,6 +90,10 @@
<#if attribute.annotations.inputTypeMax??>max="${attribute.annotations.inputTypeMax}"</#if>
<#if attribute.annotations.inputTypeMin??>min="${attribute.annotations.inputTypeMin}"</#if>
<#if attribute.annotations.inputTypeStep??>step="${attribute.annotations.inputTypeStep}"</#if>
<#if attribute.annotations.inputTypeStep??>step="${attribute.annotations.inputTypeStep}"</#if>
<#list attribute.html5DataAnnotations as key, value>
data-${key}="${value}"
</#list>
/>
</#macro>