<div id="field-13380-10503" class="field">
<label class="field__label" for="field-13380-10503-control">
<span class="label">Text-Feld<span aria-hidden="true" class="label__required" title="Notwendig">*</span></span>
</label>
<div id="field-13380-10503-errors" class="field__errors" hidden>
<div class="notice notice--error">
<svg class="icon icon--caution notice__icon" viewBox="0 0 200 200" aria-hidden="true">
<use xlink:href="/assets/icons/icons.3bafb6df0d.svg#caution"></use>
</svg>
<span class="notice__text"></span>
</div>
</div>
<div class="field__controls field__controls--stacked">
<div class="field__control">
<div class="input">
<div class="input__inner">
<input class="input__input input__input--text" id="field-13380-10503-control" name="text-field" required aria-describedby="field-13380-10503-instructions" aria-required="true" type="text" />
</div>
</div>
</div>
</div>
<div class="field__footer">
<div class="field__instructions" id="field-13380-10503-instructions">
<div class="notice notice--instructions">
<svg class="icon icon--info-alt notice__icon" viewBox="0 0 200 200" aria-hidden="true">
<use xlink:href="/assets/icons/icons.3bafb6df0d.svg#info-alt"></use>
</svg>
<span class="notice__text">Das ist eine Beschreibung</span>
</div>
</div>
</div>
</div>
{% set id = id ??? html_id('field') %}
{% set type = type ??? 'unknown' %}
{% set stacked = stacked ?? true %}
{% set hidden = hidden ?? false %}
{% set repeatable = repeatable ?? false %}
{% set required = required ?? false %}
{% set controls = controls ?? false %}
{% set controls = control|default ? [control] : (controls is not list ? [controls] : controls) %}
{% set multiple = controls|length > 1 or groups|default or newGroup|default %}
{% set tag = multiple ? 'fieldset' : 'div' %}
{% set labelTag = multiple ? 'legend' : 'label' %}
{% set labelPosition = labelPosition ?? 'above' %}
{% set subfieldLabelPosition = subfieldLabelPosition ?? 'above' %}
{% set instructionsPosition = instructionsPosition ?? 'below' %}
{% set descriptionId = 'instructions' | namespaceInputId(id) %}
{% set errors = errors ?? [] %}
{% set errorsId = 'errors' | namespaceInputId(id) %}
{% set describedBy = html_classes({
(descriptionId): instructions ?? false,
(errorsId): errors|length > 0,
}) %}
<{{ tag }} {{ html_attributes({
id: id,
class: 'field',
hidden: hidden,
}, attrs ?? {}) }}>
{% if label|default %}
<{{ labelTag }} {{ html_attributes({
class: ['field__label', labelPosition == 'hidden' ? 'u-hidden-visually'],
for: labelTag == 'label' ? 'control' | namespaceInputId(id),
}) }}>
{% include '@label' with {
text: label,
required: required,
} only %}
</{{ labelTag }}>
{% endif %}
{% if instructions|default and instructionsPosition == 'above' %}
<div class="field__instructions" id="{{ descriptionId }}">
{% include '@notice' with {
type: 'instructions',
text: instructions,
} only %}
</div>
{% endif %}
<div {{ html_attributes({
id: errorsId,
class: 'field__errors',
hidden: errors|length == 0,
'data-error-key': errorKey ?? false,
}) }}>
{% include '@notice' with {
type: 'error',
text: errors | join('<br>'),
} only %}
</div>
{% for name, value in hiddenInputs|default %}
{{ hiddenInput(name, value.value ?? value, value.attrs ?? {}) }}
{% endfor %}
{% if html|default %}
{{ html | componentize }}
{% endif %}
{% if groups|default or newGroup|default %}
<div class="field__groups">
{% macro group(group, id, deletable = false, withBorder = true) %}
{% set groupId = group.id | default(html_id('group')) | namespaceInputId(id) %}
<div {{ html_attributes({
id: groupId,
class: {
'field__group': true,
'field__group--with-counter': deletable,
'field__group--with-border': withBorder,
'field__group--inline': not withBorder,
},
}) }}>
{% if deletable %}
<div class="field__group-delete">
{% include '@icon-button' with {
icon: 'cross',
text: 'Delete row' | t('site'),
attrs: {
'data-field-delete-group': true,
},
} only %}
</div>
{% endif %}
{% for name, value in group.hiddenInputs|default([]) %}
{{ hiddenInput(name, value.value ?? value, value.attrs ?? {}) }}
{% endfor %}
{% for row in group.fields %}
{% set rowId = loop.index %}
<div class="field__group-row">
{% for subfield in row %}
{% include '@field' with subfield | merge({
id: subfield.id | default("#{rowId}-#{loop.index}") | namespaceInputId(groupId),
attrs: {
class: 'field__group-field',
},
}) only %}
{% endfor %}
</div>
{% endfor %}
</div>
{% endmacro %}
{% for group in groups %}
{{ _self.group(group, id, repeatable, labelPosition != 'hidden') }}
{% endfor %}
</div>
{% if newGroup|default %}
<div class="field__add-group">
{% include '@icon-button' with {
icon: 'plus',
text: newGroup.label,
attrs: {
'data-field-add-group': 'template' | namespaceInputId(id) | namespaceInputId(),
'data-field-add-group-max': repeatable.max ?? null,
},
} only %}
</div>
<template id="{{ 'template' | namespaceInputId(id) }}">
{{ _self.group(newGroup, id, repeatable, labelPosition != 'hidden') }}
</template>
{% endif %}
{% elseif controls|default %}
<div {{ html_attributes({
class: {
'field__controls': true,
'field__controls--stacked': stacked,
'field__controls--not-narrow': stacked is same as('not-narrow'),
},
}) }}>
{% for control in controls %}
<div class="field__control">
{% if control.use|default %}
{% include control.use with control | withoutKey('use') | merge({
id: (multiple ? "control-#{loop.index}" : 'control') | namespaceInputId(id),
describedBy: describedBy,
required: control.required ?? required ?? false,
invalid: invalid ?? errors|length > 0,
name: control.name ?? name ?? false,
}) only %}
{% else %}
{{ control.html | default('') | raw }}
{% endif %}
</div>
{% endfor %}
</div>
{% endif %}
<div class="field__footer">
{% if instructions|default and instructionsPosition == 'below' %}
<div class="field__instructions" id="{{ descriptionId }}">
{% include '@notice' with {
type: 'instructions',
text: instructions,
} only %}
</div>
{% endif %}
{% if action|default %}
<div class="field__action">
{% include '@icon-button' with action only %}
</div>
{% endif %}
</div>
</{{ tag }}>
{
"label": "Text-Feld",
"instructions": "Das ist eine Beschreibung",
"required": true,
"type": "text",
"name": "text-field",
"control": {
"use": "@input",
"type": "text"
}
}
:root {
--field-add-group-margin-block: 2rem;
--field-controls-column-gap: 2rem;
--field-controls-row-gap: 1.2rem;
--field-errors-background-color: var(--error-color);
--field-errors-color: var(--color-white);
--field-errors-font-size: 1.4rem;
--field-errors-margin-block: 1.2rem;
--field-errors-padding-block: 0.8rem;
--field-errors-padding-inline: 1.2rem;
--field-groups-column-gap: var(--gap);
--field-groups-margin-block: 2rem;
--field-groups-row-gap: 3rem;
--field-instructions-margin-block: 0.7rem;
--field-label-margin-block: 1rem;
--field-group-background-color: transparent;
--field-group-border-color: var(--color-cyan-light);
--field-group-border-radius: var(--form-border-radius);
--field-group-counter-color: var(--form-color);
--field-group-counter-size: 2.4rem;
--field-group-delete-margin-block: 2rem;
--field-group-delete-size: 3rem;
--field-group-padding-block: 2rem;
--field-group-padding-inline: 2rem;
}
.field {
> :last-child {
margin-block-end: 0;
}
}
.field--hidden {
display: none;
}
.field__label {
display: block;
float: left;
inline-size: 100%;
margin-block-end: var(--field-label-margin-block);
padding-inline: 0;
+ * {
clear: both;
}
}
.field__errors {
--notice-color: var(--field-errors-color);
--notice-icon-color: var(--field-errors-color);
background-color: var(--field-errors-background-color);
border-radius: var(--field-errors-border-radius);
font-size: var(--field-errors-font-size);
margin-block-end: var(--field-errors-margin-block);
padding-block: var(--field-errors-padding-block);
padding-inline: var(--field-errors-padding-inline);
}
.field__controls {
clear: both;
column-gap: var(--field-controls-column-gap);
display: flex;
flex-direction: row;
flex-wrap: wrap;
row-gap: var(--field-controls-row-gap);
}
.field__controls--not-narrow {
row-gap: calc(var(--field-controls-row-gap) * 2);
}
.field__controls--stacked {
flex-direction: column;
flex-wrap: nowrap;
}
.field__control {
--horizontal-rule-margin-block: var(--field-groups-row-gap);
flex-grow: 1;
min-inline-size: min-content;
}
.field__groups {
clear: both;
counter-reset: field-groups;
margin-block-end: var(--field-groups-margin-block);
}
.field__groups:not(:has(.field__group)) {
display: none;
}
.field__group {
--label-font-weight: var(--font-weight-regular);
position: relative;
& + & {
margin-block-start: var(--field-groups-row-gap);
}
}
.field__group--inline {
--label-font-weight: var(--font-weight-bold);
}
.field__group--with-border .field__group--inline {
--label-font-weight: var(--font-weight-regular);
}
.field__group--with-border {
background-color: var(--field-group-background-color);
border: 3px solid var(--field-group-border-color);
border-radius: var(--field-group-border-radius);
counter-increment: field-groups;
margin-block-end: var(--field-groups-row-gap);
padding-block: var(--field-group-padding-block);
padding-inline: var(--field-group-padding-inline);
}
.field__group--with-counter {
&::before {
block-size: var(--field-group-counter-size);
border: 1px solid var(--field-group-counter-color);
border-radius: 50%;
color: var(--field-group-counter-color);
content: counter(field-groups);
font-size: calc(var(--field-group-counter-size) * 0.6);
inline-size: var(--field-group-counter-size);
inset-block-start: var(--field-group-padding-block);
inset-inline-start: var(--field-group-padding-inline);
line-height: calc(var(--field-group-counter-size) - 2px);
position: absolute;
text-align: center;
}
}
.field__group-row {
display: grid;
gap: var(--field-groups-column-gap);
grid-auto-columns: 1fr;
grid-auto-flow: column;
& + & {
margin-block-start: var(--field-groups-row-gap);
}
}
.field__group-delete {
--icon-button-size: var(--field-group-delete-size);
margin-block-end: var(--field-group-delete-margin-block);
text-align: end;
}
.field__add-group {
--icon-button-size: 3rem;
}
.field__footer {
display: flex;
gap: 2rem;
justify-content: space-between;
}
.field__instructions {
--notice-font-size: 1.4rem;
margin-block-end: var(--field-instructions-margin-block);
}
.field__action {
--icon-button-background-color-active: var(--field-action-icon-button-background-color--active, var(--color-midnight));
--icon-button-background-color: var(--field-action-icon-button-background-color, var(--color-orange));
--icon-button-border-color-active: transparent;
--icon-button-border-color: transparent;
--icon-button-font-weight: var(--font-weight-semibold);
--icon-button-gap: 1.2rem;
--icon-button-icon-color-active: var(--field-action-icon-button-color--active, var(--color-white));
--icon-button-icon-color: var(--field-action-icon-button-color, var(--color-midnight));
--icon-button-icon-size: 1.4rem;
--icon-button-size: 2.2rem;
--icon-button-text-size: 1.4rem;
--icon-button-underline-focus-color: currentColor;
--icon-button-underline-height: 2px;
margin-block-end: var(--field-instructions-margin-block);
margin-inline-start: auto;
}
import { on } from 'delegated-events';
import abort from '../../../javascripts/utils/abort';
import moveFocus from '../../../javascripts/utils/moveFocus';
const rowCounter = new WeakMap<HTMLElement, number>();
const getNextCount = ($element: HTMLElement): number => {
if (!rowCounter.has($element)) {
rowCounter.set($element, 0);
}
const newCount = (rowCounter.get($element) as number) + 1;
rowCounter.set($element, newCount);
return newCount;
};
const updateAddGroupButton = ($button: HTMLButtonElement) => {
const { fieldAddGroupMax: max } = $button.dataset;
const $field = $button.closest<HTMLElement>('.field') ?? abort();
const $fieldGroups = $field.querySelector('.field__groups') ?? abort();
if (max) {
$button.disabled =
$fieldGroups.querySelectorAll('.field__group').length >=
parseInt(max, 10);
}
};
on('change', 'input[data-field-select-all]', (event) => {
const { currentTarget: $selectAll } = event;
$selectAll
.closest('.field')
?.querySelectorAll<HTMLInputElement>('[type="checkbox"]')
.forEach(($checkbox) => {
if ($checkbox !== $selectAll) {
$checkbox.checked = $selectAll.checked;
}
});
});
on('click', 'button[data-field-add-group]', (event) => {
const { currentTarget: $trigger } = event;
const { fieldAddGroup: templateId = '' } = $trigger.dataset;
const $template = document.getElementById(templateId) ?? abort();
const $field = $trigger.closest<HTMLElement>('.field') ?? abort();
const $fieldGroups = $field.querySelector('.field__groups') ?? abort();
const $tempEl = document.createElement('div');
$tempEl.innerHTML = $template.innerHTML.replace(
/__ROW__/g,
`new${getNextCount($field)}`,
);
const $newGroup =
$tempEl.querySelector<HTMLElement>(':first-of-type') ?? abort();
$fieldGroups.appendChild($newGroup);
moveFocus($newGroup);
updateAddGroupButton($trigger);
$newGroup.dispatchEvent(new CustomEvent('rerendered', { bubbles: true }));
});
on('click', 'button[data-field-delete-group]', (event) => {
const { currentTarget: $trigger } = event;
const $target = $trigger.closest('.field__group') ?? abort();
const $addGroupButton = $trigger
.closest<HTMLElement>('.field')
?.querySelector<HTMLButtonElement>('[data-field-add-group]');
$target.remove();
if ($addGroupButton) {
moveFocus($addGroupButton);
updateAddGroupButton($addGroupButton);
}
});
No notes defined.