<div class="input">
    <div class="input__inner">
        <input class="input__input input__input--tags" id="input" name="input" type="tags" data-tags="[{&quot;id&quot;:1,&quot;value&quot;:&quot;Welt&quot;},{&quot;id&quot;:2,&quot;value&quot;:&quot;Hund&quot;},{&quot;id&quot;:3,&quot;value&quot;:&quot;Katze&quot;},{&quot;id&quot;:4,&quot;value&quot;:&quot;Auto&quot;},{&quot;id&quot;:5,&quot;value&quot;:&quot;Himmel&quot;},{&quot;id&quot;:6,&quot;value&quot;:&quot;Sonne&quot;},{&quot;id&quot;:7,&quot;value&quot;:&quot;Weltall&quot;},{&quot;id&quot;:8,&quot;value&quot;:&quot;Weitsicht&quot;}]" data-tags-remove-label="Schlagwort löschen" />

    </div>
</div>
{% set type = type ?? 'text' %}
{% set value = value ?? null %}
{% set placeholder = placeholder ?? null %}
{% set autocomplete = autocomplete ?? null %}
{% set required = required ?? false %}
{% set disabled = disabled ?? false %}
{% set invalid = invalid ?? false %}
{% set multiple = multiple ?? false %}
{% set readonly = readonly ?? false %}
{% set limitType = limit.type ?? 'characters' %}
{% set limit = limit.count ?? limit ?? false -%}

{% set inputAttributes = {
  class: {
    'input__input': true,
    ("input__input--#{type}"): true,
  },
  id: id,
  name: name ?? false,
  autocomplete: autocomplete,
  multiple: multiple,
  required: required ? true : false,
  'aria-describedby': describedBy ?? false,
  'aria-invalid': invalid ? 'true' : null,
  'aria-required': required ? 'true' : null,
  disabled: disabled ? true : false,
  readonly: readonly ? true : false,
} -%}

<div {{ html_attributes({
  class: {
    'input': true,
  },
}, attrs ?? {}) }}>
  <div class="input__inner">
    {%- if icon|default %}
      {% include '@icon' with {
        icon: icon,
        class: 'input__icon',
      } only %}
    {% endif %}

    {% switch type %}
      {%- case 'textarea' %}
        <textarea {{ html_attributes(inputAttributes | merge({
          rows: rows ?? 4,
          placeholder: placeholder,
          maxlength: maxlength ?? false,
          'data-limit': limit ? 'limit' | namespaceInputId(id),
          'data-limit-count': limit,
          'data-limit-type': limit ? limitType,
        }), inputAttrs ?? {}) }}>{{ value }}</textarea>

      {%- case 'richtext' %}
        <input {{ html_attributes(inputAttributes | merge({
          type: 'hidden',
          value: value,
          'data-richtext': 'richtext' | namespaceInputId(id),
        }), inputAttrs ?? {}) }}>

        <div class="input__input input__input--richtext" id="{{ 'richtext' | namespaceInputId(id) }}">
          <div class="input__richtext-toolbar">
            {% set richtextActions = {
              bold: {
                title: 'Bold' | t('site'),
                icon: 'bold',
              },
              italic: {
                title: 'Italic' | t('site'),
                icon: 'italic',
              },
              underline: {
                title: 'Underline' | t('site'),
                icon: 'underline',
              },
              strikethrough: {
                title: 'Strikethrough' | t('site'),
                icon: 'strikethrough',
              },
              heading1: {
                title: 'Format as headline level 1' | t('site'),
                icon: 'heading-1',
              },
              heading2: {
                title: 'Format as headline level 2' | t('site'),
                icon: 'heading-2',
              },
              paragraph: {
                title: 'Format as paragraph' | t('site'),
                icon: 'paragraph',
              },
              quote: {
                title: 'Format as quote' | t('site'),
                icon: 'blockquote',
              },
              ulist: {
                title: 'Unordered list' | t('site'),
                icon: 'unordered-list',
              },
              olist: {
                title: 'Ordered list' | t('site'),
                icon: 'ordered-list',
              },
              code: {
                title: 'Format as code' | t('site'),
                icon: 'code',
              },
              line: {
                title: 'Horizontal line' | t('site'),
                icon: 'line',
              },
              link: {
                title: 'Link' | t('site'),
                icon: 'link',
              },
              image: {
                title: 'Image' | t('site'),
                icon: 'image',
              },
              clear: {
                title: 'Clear formatting' | t('site'),
                icon: 'clear',
              },
              alignleft: {
                title: 'Align to the left' | t('site'),
                icon: 'align-left',
              },
              alignright: {
                title: 'Align to the right' | t('site'),
                icon: 'align-right',
              },
              aligncenter: {
                title: 'Align centered' | t('site'),
                icon: 'align-center',
              },
            } %}

            {% for button in buttons %}
              {% if button in richtextActions|keys %}
                <button {{ html_attributes({
                  class: 'input__richtext-toolbar-button',
                  type: 'button',
                  title: richtextActions[button].title,
                  'data-exec': button,
                }) }}>
                  {% include '@icon' with {
                    icon: richtextActions[button].icon,
                    class: 'input__richtext-toolbar-button-icon',
                  } only %}
                </button>
              {% endif %}
            {% endfor %}
          </div>

          <div class="input__richtext-content">&nbsp;</div>
        </div>

      {%- case 'select' %}
        <select {{ html_attributes(inputAttributes, inputAttrs ?? {}) }}>
          {% set hasOptgroups = false %}

          {%- if required and selectValue is defined and selectValue %}
            <option class="input__option input__option--select" disabled selected value="">
              {{- selectValue -}}
            </option>
          {% endif -%}

          {% if resetValue is defined and resetValue %}
            <option class="input__option input__option--reset" value="">
              {{- resetValue -}}
            </option>
          {% endif %}

          {%- for option in options %}
            {% if option.optgroup is defined %}
              {% if hasOptgroups %}
                </optgroup>
              {% else %}
                {% set hasOptgroups = true %}
              {% endif %}

              <optgroup class="input__option-group" label="{{ option.optgroup }}">
            {% else %}
              <option {{ html_attributes({
                class: 'input__option',
                value: option.value,
                selected: option.selected ?? false,
                disabled: option.disabled ?? false,
              }) }}>
                {{- option.label -}}
              </option>
            {% endif %}
          {% endfor -%}

          {% if hasOptgroups %}
            </optgroup>
          {% endif %}
        </select>

        {%- if not multiple %}
          {% include '@icon' with {
            icon: 'caret-down',
            class: 'input__icon input__icon--select',
          } only %}
        {% endif %}

      {%- case 'digit-code' %}
        <div class="input__digit-code">
          <div class="input__digit-code-input"{% if length|default %} style="--input-digit-code-length: {{ length }}"{% endif %}>
            <input {{ html_attributes(inputAttributes | merge({
              value: value,
              type: 'text',
              placeholder: placeholder,
              maxlength: length ?? null,
              inputmode: 'decimal',
              pattern: '[0-9]*',
            }), inputAttrs ?? {}) }} />
          </div>
        </div>

      {%- default %}
        <input {{ html_attributes(inputAttributes | merge({
          value: type != 'file' ? value,
          type: type,
          placeholder: placeholder,
          maxlength: maxlength ?? false,
          min: min ?? false,
          max: max ?? false,
          step: step ?? false,
          multiple: multiple ?? false,
          accept: accept ?? false,
          inputmode: inputmode ?? false,
          spellcheck: type == 'password' ? 'false' : (spellcheck ?? false),
          'data-limit': limit ? 'limit' | namespaceInputId(id),
          'data-limit-count': limit,
          'data-limit-type': limit ? limitType,
          'data-tags': tags|default ? tags|json_encode,
          'data-tags-remove-label': tags|default ? 'Remove Tag' | t('site'),
          'data-select-on-click': selectOnClick ?? false,
          'data-tel': type == 'tel' and countries|default ? countries | merge({
            i18n: {
              selectedCountryAriaLabel: 'Selected country' | t('site'),
              noCountrySelected: 'No country selected' | t('site'),
              countryListAriaLabel: 'List of countries' | t('site'),
              searchPlaceholder: 'Search' | t('site'),
              zeroSearchResults: 'No results found' | t('site'),
              oneSearchResult: '1 result found' | t('site'),
              multipleSearchResults: '${count} results found' | t('site'),
            },
          }),
          'data-typeahead': typeahead|default ? {
            id: 'typeahead' | namespaceInputId(id),
            url: typeahead.url,
            minLength: typeahead.minLength ?? 0,
          },
          autocomplete: typeahead|default ? 'off' : autocomplete,
        }), inputAttrs ?? {}) }} />

      {% if type == 'file' %}
        <ul class="input__uploads">
          {% for file in value %}
            <li class="input__upload">{{ file }}</li>
          {% endfor %}
        </ul>
      {% endif %}

    {%- endswitch -%}

    {%- if limit and (type == 'textarea' or type == 'text') -%}
      <span class="input__limit" id="{{ 'limit' | namespaceInputId(id) }}">
        {%- if limitType == 'words' -%}
          {{ 'Words left: <strong>{words}</strong>' | t('site', {
            words: limit - value|default('')|words_count,
          }) | raw }}
        {%- elseif limitType == 'characters' -%}
          {{ 'Characters left: <strong>{chars}</strong>' | t('site', {
            chars: limit - value|default('')|chars_count,
          }) | raw }}
        {%- endif -%}
      </span>
    {%- endif -%}
  </div>
</div>
{
  "id": "input",
  "name": "input",
  "type": "tags",
  "tags": [
    {
      "id": 1,
      "value": "Welt"
    },
    {
      "id": 2,
      "value": "Hund"
    },
    {
      "id": 3,
      "value": "Katze"
    },
    {
      "id": 4,
      "value": "Auto"
    },
    {
      "id": 5,
      "value": "Himmel"
    },
    {
      "id": 6,
      "value": "Sonne"
    },
    {
      "id": 7,
      "value": "Weltall"
    },
    {
      "id": 8,
      "value": "Weitsicht"
    }
  ]
}
  • Content:
    :root {
      --input-background-color: var(--color-white);
      --input-border-color-invalid: var(--error-color);
      --input-border-color: var(--form-color);
      --input-border-radius: var(--form-border-radius);
      --input-border-width: 2px;
      --input-color: var(--text-color);
      --input-dropdown-background-selected: var(--color-grey-light);
      --input-dropdown-background: var(--color-white);
      --input-dropdown-border-color: var(--form-color);
      --input-dropdown-color: var(--text-color);
      --input-dropdown-padding-block: 1.2rem;
      --input-focus-outline-color: var(--form-focus-color);
      --input-focus-outline-width: 3px;
      --input-font-size: 1.8rem;
      --input-icon-color: var(--form-color);
      --input-icon-gap: 0.6rem;
      --input-icon-size: 1.6rem;
      --input-limit-background-over-limit: var(--error-color);
      --input-limit-background: var(--form-color);
      --input-limit-color-over-limit: var(--color-white);
      --input-limit-color: var(--color-white);
      --input-limit-size: 1.3rem;
      --input-line-height: 2.4rem;
      --input-padding-block: 1.2rem;
      --input-padding-inline: 2rem;
      --input-placeholder-color: var(--form-placeholder-color);
      --input-richtext-button-background-color-engaged: var(--form-color);
      --input-richtext-button-background-color-selected: var(--form-color);
      --input-richtext-button-background-color: var(--color-white);
      --input-richtext-button-border-color-engaged: var(--form-color);
      --input-richtext-button-border-color-selected: var(--form-color);
      --input-richtext-button-border-color: var(--form-color);
      --input-richtext-button-color-engaged: var(--color-white);
      --input-richtext-button-color-selected: var(--color-white);
      --input-richtext-button-color: var(--form-color);
      --input-richtext-button-size: 2.6rem;
      --input-tag-background: var(--form-color);
      --input-tag-color: var(--color-white);
      --input-tag-remove-background: var(--color-white);
      --input-tag-remove-color: var(--form-color);
      --input-uploads-background-color: var(--background-color);
      --input-uploads-color: var(--text-color);
      --input-uploads-gap: 1.5rem;
      --input-uploads-margin-block: 1.2rem;
      --input-z-index: #{z-index('default')};
    }
    
    .input {
      display: flex;
      flex-direction: column;
    }
    
    .input__inner {
      position: relative;
      z-index: var(--input-z-index);
    }
    
    .input__input {
      --focus-outline-offset: 0;
      --focus-outline-width: var(--input-focus-outline-width);
      --focus-outline-color: var(--input-focus-outline-color);
      --input-padding-inline-start-icon: calc(var(--input-padding-inline) + var(--input-icon-gap) + var(--input-icon-size));
      --selection-background-color: var(--input-color);
      --selection-foreground-color: var(--input-background-color);
    
      background-color: var(--input-background-color);
      border: var(--input-border-width) solid var(--input-border-color);
      border-radius: var(--input-border-radius);
      color: var(--input-color);
      display: block;
      font-size: var(--input-font-size);
      inline-size: 100%;
      line-height: var(--input-line-height);
      min-block-size: calc(var(--input-line-height) + (var(--input-padding-block) + var(--input-border-width)) * 2);
      min-inline-size: 100%;
      padding-block: var(--input-padding-block);
      padding-inline: var(--input-padding-inline);
      position: relative;
      text-align: start;
      transition-property: border-color;
      z-index: 2;
    
      &:where(:not(:first-child)) {
        padding-inline-start: var(--input-padding-inline-start-icon);
      }
    
      &:where(:not(:last-child)) {
        padding-inline-end: var(--input-padding-inline-start-icon);
      }
    
      &:is([aria-invalid='true']) {
        border-color: var(--input-border-color-invalid);
      }
    
      &:is(:focus, :focus-within) {
        border-color: var(--input-border-color);
    
        @include use-focus-outline();
      }
    
      &[disabled] {
        opacity: 0.5;
        user-select: none;
      }
    
      &::placeholder {
        color: var(--input-placeholder-color);
        opacity: 1;
      }
    }
    
    .input__input--typeahead {
      background-color: transparent;
      z-index: 2;
    }
    
    .input__input--richtext {
      padding-inline: var(--input-padding-inline);
    
      &:focus-within {
        @include use-focus-outline();
      }
    }
    
    .input__input--color {
      block-size: var(--input-line-height);
      cursor: pointer;
      line-height: 0;
    
      &::-webkit-color-swatch,
      &::-moz-color-swatch {
        border: 0;
      }
    }
    
    .input__input--select {
      cursor: pointer;
    
      &:invalid {
        color: var(--input-placeholder-color);
      }
    }
    
    .input__digit-code {
      --focus-outline-offset: 0;
      --focus-outline-width: var(--input-focus-outline-width);
      --focus-outline-color: var(--input-focus-outline-color);
      --input-digit-code-letter-spacing: 2em;
    
      background-color: var(--input-background-color);
      border: var(--input-border-width) solid var(--input-border-color);
      border-radius: var(--input-border-radius);
      container-name: input-digit-code;
      container-type: inline-size;
    
      &:has([aria-invalid='true']) {
        border-color: var(--input-border-color-invalid);
      }
    
      &:has(:focus-visible) {
        @include use-focus-outline();
      }
    }
    
    .input__digit-code-input {
      --input-line-height: 1.33;
    
      block-size: calc(var(--input-line-height) * var(--input-font-size) + var(--input-padding-block) * 2);
      font-size: var(--input-font-size);
      line-height: var(--input-line-height);
      overflow: hidden;
      position: relative;
    
      &:is([style]) {
        --input-line-height: 1.2;
    
        max-inline-size: calc((0.97ch + var(--input-digit-code-letter-spacing)) * var(--input-digit-code-length) - 1px);
    
        @supports (width: 1cqi) {
          --input-font-size: calc(38cqi / var(--input-digit-code-length));
        }
      }
    
      @supports (height: 1lh) {
        block-size: calc(1lh + var(--input-padding-block) * 2);
      }
    }
    
    .input__input--digit-code {
      --focus-outline-color: transparent;
      --focus-outline-width: 0;
      --input-border-width: 0;
    
      background-color: transparent;
      background-image: linear-gradient(to right, var(--color-grey-light) 1px, transparent 1px);
      background-origin: content-box;
      background-position-x: calc((var(--input-digit-code-letter-spacing) / 2) + 0.97ch);
      background-position-y: center;
      background-repeat: repeat-x;
      background-size: calc(0.97ch + var(--input-digit-code-letter-spacing)) 1.3em;
      font-variant-numeric: tabular-nums slashed-zero;
      inline-size: auto;
      inset-block: 0;
      inset-inline-end: -5ch;
      inset-inline-start: -1px;
      letter-spacing: var(--input-digit-code-letter-spacing);
      max-inline-size: none;
      padding-inline: calc((var(--input-digit-code-letter-spacing) / 2));
      position: absolute;
    }
    
    .input__input--tags {
      display: flex;
      flex-wrap: wrap;
      gap: 1rem;
      padding-inline: var(--input-padding-inline);
    
      &:focus-within {
        @include use-focus-outline();
      }
    }
    
    .input__richtext-toolbar {
      display: flex;
      gap: 0.5rem;
      user-select: none;
    }
    
    .input__richtext-toolbar-button {
      background-color: var(--input-richtext-button-background-color);
      block-size: var(--input-richtext-button-size);
      border: 1px solid var(--input-richtext-button-border-color);
      border-radius: 0.5rem;
      color: var(--input-richtext-button-color);
      font-size: calc(var(--input-richtext-button-size) * 0.5);
      inline-size: var(--input-richtext-button-size);
      line-height: 0;
      transition-property: background-color, border-color, color;
    
      &:is(:hover, :focus-visible) {
        background-color: var(--input-richtext-button-background-color-engaged);
        border-color: var(--input-richtext-button-border-color-engaged);
        color: var(--input-richtext-button-color-engaged);
      }
    }
    
    .input__richtext-toolbar-button--selected {
      background-color: var(--input-richtext-button-background-color-selected);
      border-color: var(--input-richtext-button-border-color-selected);
      color: var(--input-richtext-button-color-selected);
    }
    
    .input__richtext-toolbar-button-icon {
      pointer-events: none;
    }
    
    .input__richtext-content {
      --focus-outline-width: 0;
    
      color: var(--text-color);
      line-height: var(--line-height-body);
      margin-block-start: 1rem;
    
      > * + * {
        margin-block-start: 0.5rem;
      }
    
      :is(h1) {
        font-size: 1.3em;
        font-weight: bold;
      }
    
      :is(h2) {
        font-size: 1.1em;
        font-weight: bold;
      }
    
      :is(blockquote) {
        border-inline-start: 0.5rem solid var(--color-gray-500);
        padding-inline-start: 1.5rem;
      }
    
      :is(ul, ol) {
        padding-inline-start: 2rem;
      }
    
      :is(pre) {
        background-color: var(--light-background-color);
        font-family: monospace;
        white-space: nowrap;
      }
    
      :is(hr) {
        border-block-start: 1px solid var(--text-color);
      }
    
      :is(a) {
        color: var(--link-color);
        text-decoration-color: var(--link-color);
        text-decoration-line: underline;
        text-decoration-style: solid;
        text-decoration-thickness: 1px;
      }
    }
    
    .input__option,
    .input__option-group {
      color: var(--input-color);
      font-family: inherit;
    }
    
    .input__option--select {
      color: var(--input-placeholder-color);
    }
    
    .input__icon {
      color: var(--input-icon-color);
      font-size: var(--input-icon-size);
      inset-block-start: 50%;
      inset-inline-start: var(--input-padding-inline);
      line-height: 0;
      pointer-events: none;
      position: absolute;
      transform: translateY(-50%);
      transition-property: color;
      z-index: 3;
    }
    
    .input__icon--select {
      inset-inline: auto var(--input-padding-inline);
    }
    
    .input__limit {
      --input-limit-start-offset: calc(var(--input-border-radius) + 0.4em);
    
      align-self: flex-end;
      background-color: var(--input-limit-background);
      color: var(--input-limit-color);
      display: inline-block;
      font-size: var(--input-limit-size);
      line-height: 1;
      margin-block-start: 0.4rem;
      padding-block: 0.4em;
      padding-inline: 0.8em;
      position: relative;
      transition-property: background-color, color;
      z-index: 1;
    
      .input__input[aria-invalid='true'] + & {
        background-color: var(--input-limit-background-over-limit);
        color: var(--input-limit-color-over-limit);
      }
    }
    
    .input__tags-input {
      --focus-outline-width: 0;
    
      display: inline-block;
      flex-grow: 1;
      min-inline-size: 11rem;
      white-space: pre-wrap;
    }
    
    .input__tag {
      align-items: center;
      background-color: var(--input-tag-background);
      border-radius: 0.5rem;
      color: var(--input-tag-color);
      display: flex;
      font-size: 1.4rem;
      gap: 0.3rem;
      line-height: 2.4rem;
      padding-inline: 0.5rem;
    }
    
    .input__tag-remove {
      block-size: 2rem;
      cursor: pointer;
      display: block;
      inline-size: 2rem;
      order: 2;
      position: relative;
      transition-property: scale;
    
      &:is(:hover, :focus) {
        scale: 1.3;
      }
    
      &::after {
        background-color: var(--input-tag-remove-background);
        border-radius: 50%;
        color: var(--input-tag-remove-color);
        content: 'X';
        font-size: 1rem;
        font-weight: bold;
        inset: 0.4rem;
        line-height: 1.2rem;
        position: absolute;
        text-align: center;
      }
    }
    
    .input__dropdown {
      --input-tags-arrow-size: 1.2rem;
      --input-tags-arrow-offset: calc(var(--input-padding-inline) - var(--input-tags-arrow-size) / 2);
    
      animation-duration: var(--duration-default);
      animation-name: opacity;
      background-color: var(--input-dropdown-background);
      border-color: var(--input-dropdown-border-color);
      border-radius: var(--input-border-radius);
      border-style: solid;
      border-width: 1px;
      box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.05);
      color: var(--input-dropdown-color);
      font-size: 1.4rem;
      inline-size: 100% !important;
      line-height: var(--line-height-small);
      position: absolute;
      text-align: start;
      transform: translateY(calc(var(--input-tags-arrow-size) / 2));
      z-index: #{z-index('dropdown')};
    
      &::before {
        block-size: 0;
        border: var(--input-tags-arrow-size) solid transparent;
        border-block-end-color: var(--input-dropdown-border-color);
        content: '';
        inline-size: 0;
        inset-block-start: 0;
        inset-inline-start: 0;
        position: absolute;
        transform: translate(var(--input-tags-arrow-offset), -100%);
        z-index: 2;
      }
    }
    
    .input__dropdown--hide {
      display: none;
    }
    
    .input__dropdown-item {
      padding-block: var(--input-dropdown-padding-block);
      padding-inline: var(--input-padding-inline);
      position: relative;
      transition-property: background-color, font-weight;
    
      & + & {
        border-block-start: 1px solid var(--input-dropdown-border-color);
      }
    
      &:first-child {
        border-start-end-radius: var(--input-border-radius);
        border-start-start-radius: var(--input-border-radius);
      }
    
      &:first-child::before {
        block-size: 0;
        border: calc(var(--input-tags-arrow-size) - 0.15rem) solid transparent;
        border-block-end-color: var(--input-dropdown-background);
        content: '';
        inline-size: 0;
        inset-block-start: 0;
        inset-inline-start: 0;
        position: absolute;
        transform: translate(calc(var(--input-tags-arrow-offset) + 0.15rem), -100%);
        transition-property: border-color;
        z-index: 4;
      }
    
      &:last-child {
        border-end-end-radius: var(--input-border-radius);
        border-end-start-radius: var(--input-border-radius);
      }
    }
    
    .input__dropdown-item--selected {
      background-color: var(--input-dropdown-background-selected);
      font-weight: bold;
      text-decoration: underline;
    
      &:first-child::before {
        border-block-end-color: var(--input-dropdown-background-selected);
      }
    }
    
    .input__uploads {
      display: flex;
      flex-wrap: wrap;
      gap: var(--input-uploads-gap);
      margin-block-start: var(--input-uploads-margin-block);
    }
    
    .input__upload {
      align-items: center;
      background-color: var(--input-uploads-background-color);
      border-radius: 0.5rem;
      color: var(--input-uploads-color);
      display: flex;
      font-size: 1.4rem;
      gap: 0.3rem;
      line-height: 2.4rem;
      padding-inline: 0.5rem;
    }
    
    .input__typeahead {
      background-color: var(--input-background-color);
      position: relative;
    }
    
    .input__typeahead-hint {
      border-color: transparent;
      border-style: solid;
      border-width: var(--input-border-width);
      color: var(--color-grey-light);
      font-size: var(--input-font-size);
      inset: 0;
      line-height: var(--input-line-height);
      margin: 0 !important;
      padding-block: var(--input-padding-block);
      padding-inline: var(--input-padding-inline);
      position: absolute;
      z-index: 1;
    }
    
  • URL: /components/raw/input/input.scss
  • Filesystem Path: src/components/1-atoms/input/input.scss
  • Size: 14 KB
  • Content:
    import { on } from 'delegated-events';
    
    on('click', 'input.input__input[data-select-on-click]', (event) => {
      const { currentTarget: $input } = event;
    
      $input.select();
      $input.setSelectionRange(0, $input.value.length);
    });
    
    on('keypress', 'input.input__input--digit-code', (event) => {
      const { key } = event;
    
      if (!key.match(/^[0-9]$/)) {
        event.preventDefault();
      }
    });
    
    on('paste', 'input.input__input--digit-code', (event) => {
      const { currentTarget: $input, clipboardData } = event;
    
      event.preventDefault();
    
      const pasteValue = clipboardData?.getData('text');
      if (pasteValue) {
        $input.value = pasteValue.trim().replace(/\D/g, '');
      }
    });
    
    document
      .querySelectorAll<
        HTMLInputElement | HTMLTextAreaElement
      >('input.input__input[data-limit], textarea.input__input[data-limit]')
      .forEach(async ($input) => {
        const { default: useInputLimits } = await import(
          /* webpackChunkName: "use-input-limits" */ './useInputLimits'
        );
        useInputLimits($input);
      });
    
    document
      .querySelectorAll<HTMLInputElement>('input.input__input[data-tel]')
      .forEach(async ($input) => {
        const { default: useIntlTelInput } = await import(
          /* webpackChunkName: "use-intl-tel-input" */ './useIntlTelInput'
        );
        useIntlTelInput($input);
      });
    
    document
      .querySelectorAll<HTMLInputElement>('input.input__input[data-richtext]')
      .forEach(async ($input) => {
        const { default: usePell } = await import(
          /* webpackChunkName: "use-pell" */ './usePell'
        );
        usePell($input);
      });
    
    document
      .querySelectorAll<HTMLInputElement>('input.input__input[data-tags]')
      .forEach(async ($input) => {
        const { default: useTags } = await import(
          /* webpackChunkName: "use-tags" */ './useTags'
        );
        useTags($input);
      });
    
    document
      .querySelectorAll<HTMLInputElement>('input.input__input[data-typeahead]')
      .forEach(async ($input) => {
        const { default: useTypeahead } = await import(
          /* webpackChunkName: "use-typeahead" */ './useTypeahead'
        );
        useTypeahead($input);
      });
    
    document
      .querySelectorAll<HTMLElement>('.input__digit-code-input')
      .forEach(($el) => {
        $el.scrollLeft = 0;
        $el.addEventListener(
          'scroll',
          () => {
            $el.scrollLeft = 0;
          },
          { passive: true },
        );
      });
    
  • URL: /components/raw/input/input.ts
  • Filesystem Path: src/components/1-atoms/input/input.ts
  • Size: 2.3 KB
  • Content:
    import abort from '../../../javascripts/utils/abort';
    import charsCount from '../../../javascripts/utils/charsCount';
    import wordsCount from '../../../javascripts/utils/wordsCount';
    
    const updateCount = (
      $input: HTMLInputElement | HTMLTextAreaElement,
      invalidOnLoad: boolean,
    ) => {
      const {
        limit,
        limitType = 'characters',
        limitCount = false,
      } = $input.dataset;
    
      if (!limit || !limitCount) {
        return;
      }
    
      requestIdleCallback(() => {
        const $limit = document.getElementById(limit) ?? abort();
        const $limitDisplay = $limit.querySelector('strong') ?? abort();
        const value = $input.isContentEditable ? $input.innerHTML : $input.value;
        const count = limitType === 'words' ? wordsCount(value) : charsCount(value);
        const left = parseInt(limitCount, 10) - count;
    
        // Update display
        $limitDisplay.innerText = left.toString();
    
        // Update aria-invalid
        if (!invalidOnLoad) {
          $input.setAttribute('aria-invalid', left < 0 ? 'true' : 'false');
        }
      });
    };
    
    const useInputLimits = ($input: HTMLInputElement | HTMLTextAreaElement) => {
      // Update on load
      const invalidOnLoad = $input.getAttribute('aria-invalid') === 'true';
      updateCount($input, invalidOnLoad);
    
      // Update on keydown, change and paste
      const eventListener = () => updateCount($input, invalidOnLoad);
      $input.addEventListener('keydown', eventListener);
      $input.addEventListener('change', eventListener);
      $input.addEventListener('paste', eventListener);
    
      // Deregister function
      return () => {
        $input.removeEventListener('keydown', eventListener);
        $input.removeEventListener('change', eventListener);
        $input.removeEventListener('paste', eventListener);
      };
    };
    
    export default useInputLimits;
    
  • URL: /components/raw/input/useInputLimits.ts
  • Filesystem Path: src/components/1-atoms/input/useInputLimits.ts
  • Size: 1.7 KB
  • Content:
    import intlTelInput from 'intl-tel-input/intlTelInputWithUtils';
    import 'intl-tel-input/build/css/intlTelInput.css';
    
    const useIntlTelInput = async ($input: HTMLInputElement) => {
      if ($input.type !== 'tel') {
        return;
      }
    
      const configuration: Record<string, unknown> = JSON.parse(
        $input.dataset.tel ?? '{}',
      );
    
      intlTelInput($input, {
        i18n: configuration.i18n as {
          selectedCountryAriaLabel?: string;
          searchPlaceholder?: string;
          countryListAriaLabel?: string;
          oneSearchResult?: string;
          multipleSearchResults?: string;
          noCountrySelected?: string;
          zeroSearchResults?: string;
        },
        separateDialCode: true,
        initialCountry:
          typeof configuration.initialCountry === 'string'
            ? configuration.initialCountry.toLowerCase()
            : 'auto',
        onlyCountries: Array.isArray(configuration.onlyCountries)
          ? configuration.onlyCountries.map((i) => i.toLowerCase())
          : [],
        countryOrder: Array.isArray(configuration.countryOrder)
          ? configuration.countryOrder.map((i) => i.toLowerCase())
          : [],
      });
    };
    
    export default useIntlTelInput;
    
  • URL: /components/raw/input/useIntlTelInput.ts
  • Filesystem Path: src/components/1-atoms/input/useIntlTelInput.ts
  • Size: 1.1 KB
  • Content:
    import { init, exec } from 'pell';
    import abort from '../../../javascripts/utils/abort';
    
    const usePell = async ($input: HTMLInputElement) => {
      const { richtext } = $input.dataset;
    
      if (!richtext) {
        return;
      }
    
      const $richtext = document.getElementById(richtext) ?? abort();
    
      const actions: pell.pellAction[] = [];
      $richtext
        .querySelectorAll<HTMLButtonElement>('.input__richtext-toolbar-button')
        .forEach(($button) => {
          const { exec: name } = $button.dataset;
          const mapped: Record<
            string,
            { result: () => boolean; state?: () => boolean }
          > = {
            alignleft: {
              result: () => {
                exec('justifyLeft', '');
                return true;
              },
              state: () => document.queryCommandState('justifyLeft'),
            },
            alignright: {
              result: () => {
                exec('justifyRight', '');
                return true;
              },
              state: () => document.queryCommandState('justifyRight'),
            },
            aligncenter: {
              result: () => {
                exec('justifyCenter', '');
                return true;
              },
              state: () => document.queryCommandState('justifyCenter'),
            },
          };
    
          if (name === 'clear') {
            const action: pell.pellCustomActionConfig = {
              name,
              icon: $button.innerHTML.trim(),
              title: $button.title,
              result: () => {
                const selection = window.getSelection()?.toString();
    
                if (selection) {
                  const linesToDelete = selection.split('\n').join('<br>');
                  exec('formatBlock', '<p>');
                  document.execCommand('insertHTML', false, linesToDelete);
                } else {
                  exec('formatBlock', '<p>');
                }
    
                return true;
              },
            };
    
            actions.push(action);
          } else if (name && name in mapped) {
            const action: pell.pellCustomActionConfig = {
              name,
              icon: $button.innerHTML.trim(),
              title: $button.title,
              // eslint-disable-next-line security/detect-object-injection
              ...mapped[name], // nosemgrep: eslint.detect-object-injection
            };
    
            actions.push(action);
          } else if (name) {
            const action: pell.pellActionConfig = {
              name: name as pell.pellAction,
              icon: $button.innerHTML.trim(),
              title: $button.title,
            };
    
            actions.push(action);
          }
        });
    
      $richtext.querySelector('.input__richtext-toolbar')?.remove();
      $richtext.querySelector('.input__richtext-content')?.remove();
    
      const editor = init({
        element: $richtext,
        defaultParagraphSeparator: 'p',
        actions,
        styleWithCSS: false,
        onChange: (html) => {
          $input.value = html;
        },
        classes: {
          actionbar: 'input__richtext-toolbar',
          button: 'input__richtext-toolbar-button',
          selected: 'input__richtext-toolbar-button--selected',
          content: 'input__richtext-content',
        },
      });
    
      setTimeout(() => {
        if ($input.value) {
          editor.content.innerHTML = $input.value;
          editor.content.dispatchEvent(new MouseEvent('mouseup'));
        }
      }, 1);
    
      $richtext
        .querySelectorAll('.input__richtext-toolbar-button')
        .forEach(($button) => {
          $button.addEventListener('mousedown', (e) => e.preventDefault());
          $button.addEventListener('mouseup', () => {
            setTimeout(() => {
              editor.content.dispatchEvent(new MouseEvent('mouseup'));
            }, 1);
          });
        });
    };
    
    export default usePell;
    
  • URL: /components/raw/input/usePell.ts
  • Filesystem Path: src/components/1-atoms/input/usePell.ts
  • Size: 3.6 KB
  • Content:
    import Tagify from '@yaireo/tagify';
    
    const useTags = ($input: HTMLInputElement) => {
      // Hide original input
      $input.type = 'hidden';
    
      // Get tags
      const tags = JSON.parse($input.dataset.tags ?? '[]');
      const removeTagLabel = $input.dataset.tagsRemoveLabel ?? '';
    
      return new Tagify($input, {
        whitelist: tags,
        a11y: {
          focusableTags: true,
        },
        dropdown: {
          appendTarget: $input.closest<HTMLElement>('.input'),
        },
        editTags: false,
        templates: {
          tag(tagData) {
            return `<tag title="${tagData.title || tagData.value}" contenteditable="false" spellcheck="false" tabIndex="${
              this.settings.a11y.focusableTags ? 0 : -1
            }" class="${this.settings.classNames.tag} ${tagData.class ? tagData.class : ''}" ${this.getAttributes(tagData)}>
              <span class="${this.settings.classNames.tagText}">${
                tagData[this.settings.tagTextProp] || tagData.value
              }</span>
              <x class="${this.settings.classNames.tagX}" role="button" title="${removeTagLabel}: ${
                tagData[this.settings.tagTextProp] || tagData.value
              }" tabindex="0"></x>
            </tag>`;
          },
          dropdownHeader: () => '',
          dropdownFooter: () => '',
        },
        classNames: {
          namespace: 'input__input--tags',
          tag: 'input__tag',
          input: 'input__tags-input',
          tagNoAnimation: 'input__tag--no-animation',
          tagX: 'input__tag-remove',
          tagText: 'input__tag-text',
          dropdown: 'input__dropdown',
          dropdownWrapper: 'input__dropdown-inner',
          dropdownItem: 'input__dropdown-item',
          dropdownItemActive: 'input__dropdown-item--selected',
        },
      });
    };
    
    export default useTags;
    
  • URL: /components/raw/input/useTags.ts
  • Filesystem Path: src/components/1-atoms/input/useTags.ts
  • Size: 1.7 KB
  • Content:
    import typeahead from 'typeahead-standalone';
    
    type UseTypeaheadOptions = {
      id: string;
      url: string;
      minLength?: number;
    };
    
    const useTypeahead = ($input: HTMLInputElement) => {
      const { typeahead: rawOptions = '{}' } = $input.dataset;
      const options = JSON.parse(rawOptions) as UseTypeaheadOptions;
    
      typeahead({
        input: $input,
        minLength: options.minLength ?? 0,
        preventSubmit: true,
        source: {
          remote: {
            url: options.url,
            wildcard: '__QUERY__',
            debounce: 300,
            requestOptions: {
              headers: {
                'Content-Type': 'application/json',
                'X-Requested-With': 'XMLHttpRequest',
              },
            },
          },
        },
        classNames: {
          wrapper: 'input__typeahead',
          input: 'input__input--typeahead',
          hint: 'input__typeahead-hint',
          highlight: 'input__typeahead-highlight',
          list: 'input__dropdown',
          hide: 'input__dropdown--hide',
          show: 'input__dropdown--show',
          selected: 'input__dropdown-item--selected',
          header: 'input__dropdown-header',
          footer: 'input__dropdown-footer',
          loader: 'input__dropdown-loader',
          suggestion: 'input__dropdown-item',
          group: 'input__dropdown-group',
          empty: 'input__dropdown-empty',
          notFound: 'input__dropdown-not-found',
        },
      });
    
      const $listContainer = $input
        .closest('.input__typeahead')
        ?.querySelector('.input__dropdown');
    
      const $container = $input.closest('.input');
    
      if ($listContainer && $container) {
        $container.appendChild($listContainer);
        $listContainer.addEventListener('mousedown', (event: Event) => {
          event.stopPropagation();
          event.preventDefault();
        });
      }
    };
    
    export default useTypeahead;
    
  • URL: /components/raw/input/useTypeahead.ts
  • Filesystem Path: src/components/1-atoms/input/useTypeahead.ts
  • Size: 1.7 KB

No notes defined.