<fieldset id="field-25435-77053" class="field">
    <legend class="field__label">
        <span class="label">Name<span aria-hidden="true" class="label__required" title="Notwendig">*</span></span>
    </legend>

    <div id="field-25435-77053-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__groups">

        <div id="field-25435-77053-group-21600-86256" class="field__group field__group--with-border">

            <div class="field__group-row">

                <div id="field-25435-77053-group-21600-86256-1-1" class="field field__group-field">
                    <label class="field__label" for="field-25435-77053-group-21600-86256-1-1-control">
                        <span class="label">Anrede</span>
                    </label>

                    <div id="field-25435-77053-group-21600-86256-1-1-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">
                                    <select class="input__input input__input--select" id="field-25435-77053-group-21600-86256-1-1-control" name="gender">
                                        <option class="input__option" value="">Keine Anrede</option>
                                        <option class="input__option" value="male">Herr</option>
                                        <option class="input__option" value="female">Frau</option>
                                    </select> <svg class="icon icon--caret-down input__icon input__icon--select" viewBox="0 0 200 200" aria-hidden="true">
                                        <use xlink:href="/assets/icons/icons.3bafb6df0d.svg#caret-down"></use>
                                    </svg>
                                </div>
                            </div>
                        </div>
                    </div>

                    <div class="field__footer">

                    </div>
                </div>

                <div id="field-25435-77053-group-21600-86256-1-2" class="field field__group-field">
                    <label class="field__label" for="field-25435-77053-group-21600-86256-1-2-control">
                        <span class="label">Vorname</span>
                    </label>

                    <div id="field-25435-77053-group-21600-86256-1-2-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--type" id="field-25435-77053-group-21600-86256-1-2-control" name="forename" type="type" />

                                </div>
                            </div>
                        </div>
                    </div>

                    <div class="field__footer">

                    </div>
                </div>

                <div id="field-25435-77053-group-21600-86256-1-3" class="field field__group-field">
                    <label class="field__label" for="field-25435-77053-group-21600-86256-1-3-control">
                        <span class="label">Nachname</span>
                    </label>

                    <div id="field-25435-77053-group-21600-86256-1-3-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--type" id="field-25435-77053-group-21600-86256-1-3-control" name="lastname" type="type" />

                                </div>
                            </div>
                        </div>
                    </div>

                    <div class="field__footer">

                    </div>
                </div>
            </div>
        </div>

    </div>

    <div class="field__footer">

    </div>
</fieldset>
{% 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": "Name",
  "required": true,
  "type": "name",
  "name": "name",
  "groups": [
    {
      "fields": [
        [
          {
            "name": "gender",
            "label": "Anrede",
            "control": {
              "use": "@input",
              "type": "select",
              "options": [
                {
                  "value": "",
                  "label": "Keine Anrede"
                },
                {
                  "value": "male",
                  "label": "Herr"
                },
                {
                  "value": "female",
                  "label": "Frau"
                }
              ]
            }
          },
          {
            "name": "forename",
            "control": {
              "use": "@input",
              "type": "type"
            },
            "label": "Vorname"
          },
          {
            "name": "lastname",
            "control": {
              "use": "@input",
              "type": "type"
            },
            "label": "Nachname"
          }
        ]
      ]
    }
  ]
}
  • Content:
    :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;
    }
    
  • URL: /components/raw/field/field.scss
  • Filesystem Path: src/components/2-molecules/field/field.scss
  • Size: 5.2 KB
  • Content:
    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);
      }
    });
    
  • URL: /components/raw/field/field.ts
  • Filesystem Path: src/components/2-molecules/field/field.ts
  • Size: 2.4 KB

No notes defined.