feat: Checkboxes and Radio Buttons field types (#17934)

Co-authored-by: Your Name <you@example.com>
Co-authored-by: Roman Davydchuk <roman.davydchuk@n8n.io>
This commit is contained in:
Michael Kret
2025-08-11 17:11:22 +03:00
committed by GitHub
parent f69d8efa04
commit fdab0ab116
9 changed files with 800 additions and 40 deletions

View File

@@ -295,9 +295,46 @@
}
input[type='checkbox'] {
appearance: none;
width: var(--checkbox-size);
height: var(--checkbox-size);
border: 1px solid var(--color-input-border);
border-radius: 3px;
cursor: pointer;
position: relative;
}
.multiselect-checkbox:checked {
background: var(--color-focus-border);
border-color: var(--color-focus-border);
}
.multiselect-checkbox:checked::after {
content: '✔';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: var(--font-size-label);
font-weight: bold;
}
.multiselect[data-radio-select] .multiselect-checkbox {
border-radius: 50%;
width: var(--checkbox-size);
height: var(--checkbox-size);
display: flex;
align-items: center;
justify-content: center;
}
.multiselect[data-radio-select] .multiselect-checkbox:checked {
background: white;
border-color: var(--color-focus-border);
border-width: 4px;
border-radius: 50%;
}
.multiselect[data-radio-select] .multiselect-checkbox:checked::after {
content: '';
}
/* required field ----------------------------- */
.form-required {
@@ -454,7 +491,22 @@
{{#if isMultiSelect}}
<div>
<label class='form-label {{inputRequired}}'>{{label}}</label>
<div class='multiselect {{inputRequired}}' id='{{id}}'>
<div
class='multiselect {{inputRequired}}'
id='{{id}}'
{{#if radioSelect}}
data-radio-select='{{radioSelect}}'
{{/if}}
{{#if exactSelectedOptions}}
data-exact-select='{{exactSelectedOptions}}'
{{/if}}
{{#if minSelectedOptions}}
data-min-select='{{minSelectedOptions}}'
{{/if}}
{{#if maxSelectedOptions}}
data-max-select='{{maxSelectedOptions}}'
{{/if}}
>
{{#each multiSelectOptions}}
<div class='multiselect-option'>
<input type='checkbox' class='multiselect-checkbox' id='{{id}}' />
@@ -620,6 +672,15 @@
</section>
</div>
<script>
function updateError(errorElement, action = 'add', message = '') {
if(action === 'add') {
errorElement.textContent = message;
errorElement.classList.add('error-show');
} else {
errorElement.classList.remove('error-show');
}
}
function validateInput(input, errorElement) {
const value = input.value.trim();
const type = input.type;
@@ -628,19 +689,17 @@
return validateEmailInput(value, errorElement);
} else if (type === 'number' && value !== '') {
if (isNaN(value)) {
errorElement.textContent = 'Enter only numbers in this field';
errorElement.classList.add('error-show');
updateError(errorElement, 'add', 'Enter only numbers in this field');
return false;
} else {
errorElement.classList.remove('error-show');
updateError(errorElement, 'remove');
return true;
}
} else if (value === '') {
errorElement.textContent = 'This field is required';
errorElement.classList.add('error-show');
updateError(errorElement, 'add', 'This field is required');
return false;
} else {
errorElement.classList.remove('error-show');
updateError(errorElement, 'remove');
return true;
}
}
@@ -650,12 +709,11 @@
const isValidEmail = regex.test(value);
if (!isValidEmail) {
errorElement.textContent = 'Enter a valid email address in this field';
errorElement.classList.add('error-show');
updateError(errorElement, 'add', 'Enter a valid email address in this field');
return false;
} else {
errorElement.textContent = 'This field is required';
errorElement.classList.remove('error-show');
updateError(errorElement, 'remove');
return true;
}
}
@@ -674,16 +732,41 @@
return selectedValues;
}
function validateMultiselect(input, errorElement) {
const selectedValues = getSelectedValues(input);
function getDataValues(input) {
const radio = input.dataset.radioSelect ? true : false;
const exact = input.dataset.exactSelect ? parseInt(input.dataset.exactSelect, 10) : null;
const maxSelect = input.dataset.maxSelect ? parseInt(input.dataset.maxSelect, 10) : null;
const minSelect = input.dataset.minSelect ? parseInt(input.dataset.minSelect, 10) : null;
return { radio, exact, maxSelect, minSelect };
}
if (!selectedValues.length) {
errorElement.classList.add('error-show');
function validateMultiselect(input, errorElement) {
const values = getSelectedValues(input);
const data = getDataValues(input);
const required = input.classList.contains('form-required');
if (required && !values.length) {
updateError(errorElement, 'add', 'This field is required');
return false;
} else {
errorElement.classList.remove('error-show');
return true;
}
if (data.exact && values.length !== data.exact) {
updateError(errorElement, 'add', `You must select ${data.exact} options`);
return false;
}
if (data.minSelect && values.length < data.minSelect) {
updateError(errorElement, 'add', `You must select at least ${data.minSelect} options`);
return false;
}
if(data.maxSelect && values.length > data.maxSelect) {
updateError(errorElement, 'add', `You can select a maximum of ${data.maxSelect} options`);
return false;
}
updateError(errorElement, 'remove');
return true;
}
const form = document.querySelector('#n8n-form');
@@ -726,6 +809,32 @@
const requiredInputs = document.querySelectorAll('.form-required:not(label)');
const emailInputs = document.querySelectorAll("input[type=email]");
const multiselectInputs = document.querySelectorAll('.multiselect');
multiselectInputs.forEach((input) => {
const data = getDataValues(input);
const errorElement = document.querySelector(`.error-${input.id}`);
if (data.radio) {
const checkboxes = input.querySelectorAll('.multiselect-checkbox');
checkboxes.forEach((checkbox) => {
checkbox.addEventListener('change', (event) => {
if (event.target.checked) {
checkboxes.forEach((cb) => {
if (cb !== event.target) {
cb.checked = false;
}
});
updateError(errorElement, 'remove');
}
});
});
}
input.addEventListener('click', () => {
validateMultiselect(input, errorElement);
});
});
requiredInputs.forEach((input) => {
const errorSelector = `.error-${input.id}`;
@@ -740,7 +849,7 @@
validateInput(input, error);
});
input.addEventListener('input', () => {
error.classList.remove('error-show');
updateError(error, 'remove');
});
}
});
@@ -823,7 +932,7 @@
valid.push(validateEmailInput(value, error));
});
requiredInputs.forEach((input) => {
[...requiredInputs, ...multiselectInputs].forEach((input) => {
const errorSelector = `.error-${input.id}`;
const error = document.querySelector(errorSelector);