Europa Component Library

File uploads

Combination of fields and buttons to add documents or media to a form.

When to use

When there is a need to allow users to attach anything to a form (documents, pictures, videos, etc).

Implementation

In order to automatically attach event listeners on your file upload forms, add the following script to your page:

document.addEventListener('DOMContentLoaded', function() {
  ECL.fileUploads();
});
{#
  - "id" (string): the id of the file input (default: 'default-id')
  - "name" (string): the name of the file input (default: 'default-name')
  - "value" (string): the name of the file selected (default: 'No file selected.')
  - "label_browse" (string): the label of the Browse button (default: 'Browse')
  - "has_upload" (boolean): define if form upload has a "upload" button (default: true)
  - "label_upload" (string): the label of the Upload button (default: 'Upload')
  - "is_disabled" (boolean): define if form upload is disabled (default: false)
  - "is_multiple" (boolean): define if form upload is multiple (default: false)
  - "has_error" (boolean): define if form upload has error (default: false)
  - "extra_classes" (string): extra CSS classes to be added
  - "extra_attributes" (array): extra attributes classes (optional, format: [{ 'name': 'name_of_the_attribute', 'value': 'value_of_the_attribute'}])
#}

{% set id = id|default('default-id') %}
{% set name = name|default('default-name') %}
{% set value = value|default('No file selected.') %}
{% set label_browse = label_browse|default('Browse') %}
{% set has_upload = has_upload|default(true) %}
{% set label_upload = label_upload|default('Upload') %}
{% set is_disabled = is_disabled|default(false) %}
{% set is_multiple = is_multiple|default(false) %}
{% set has_error = has_error|default(false) %}

{# Internal properties #}

{% set _css_class = 'ecl-file-upload' %}
{% set _extra_attributes = '' %}

{# Internal logic - Process properties #}

{% if has_error == true %}
  {% set _css_class = _css_class ~ ' ecl-file-upload--has-error' %}
{% endif %}

{% if is_disabled == true %}
  {% set _css_class = _css_class ~ ' ecl-file-upload--is-disabled' %}
{% endif %}

{% if extra_class is defined %}
  {% set _css_class = _css_class ~ ' ' ~ extra_class %}
{% endif %}

{% if extra_attributes is defined %}
  {% for attr in extra_attributes %}
    {% set _extra_attributes = _extra_attributes ~ ' ' ~ attr.name ~ '="' ~ attr.value ~'"' %}
  {% endfor %}
{% endif %}

{# Print the result  #}

<div class="{{ _css_class }}" {{ _extra_attributes|raw }}>
  <div class="ecl-file-upload__value" tabindex="0">{{ value }}</div>
  <label for="{{ id }}">
    <span class="ecl-file-upload__browse" role="button" aria-controls="{{ id }}" tabindex="0">{{ label_browse }}</span>
  </label>
  {% if has_upload == true and is_disabled == false %}
  <button class="ecl-file-upload__upload" tabindex="0" type="submit">
    {{ label_upload }}
  </button>
  {% endif %}
  <input class="ecl-file-upload__input" name="{{ name }}" type="file" id="{{ id }}" {% if is_disabled == true %}disabled{% endif %} {% if is_multiple == true %}multiple{% endif %}>
</div>
/* Normal file upload */
{
  "id": "example-input-id-1",
  "name": "example-input-name-1"
}

/* Disabled file upload */
{
  "id": "example-input-id-2",
  "name": "example-input-name-2",
  "is_disabled": true
}

/* Multiple file upload */
{
  "id": "example-input-id-3",
  "name": "example-input-name-3",
  "is_multiple": true
}

/* File upload with error */
{
  "id": "example-input-id-4",
  "name": "example-input-name-4",
  "has_error": true
}

<!-- Normal file upload -->
<div class="ecl-file-upload">
  <div class="ecl-file-upload__value" tabindex="0">No file selected.</div>
  <label for="example-input-id-1">
    <span class="ecl-file-upload__browse" role="button" aria-controls="example-input-id-1" tabindex="0">Browse</span>
  </label>
  <button class="ecl-file-upload__upload" tabindex="0" type="submit">
    Upload
  </button>
  <input class="ecl-file-upload__input" name="example-input-name-1" type="file" id="example-input-id-1">
</div>

<!-- Disabled file upload -->
<div class="ecl-file-upload ecl-file-upload--is-disabled">
  <div class="ecl-file-upload__value" tabindex="0">No file selected.</div>
  <label for="example-input-id-2">
    <span class="ecl-file-upload__browse" role="button" aria-controls="example-input-id-2" tabindex="0">Browse</span>
  </label>
  <input class="ecl-file-upload__input" name="example-input-name-2" type="file" id="example-input-id-2" disabled>
</div>

<!-- Multiple file upload -->
<div class="ecl-file-upload">
  <div class="ecl-file-upload__value" tabindex="0">No file selected.</div>
  <label for="example-input-id-3">
    <span class="ecl-file-upload__browse" role="button" aria-controls="example-input-id-3" tabindex="0">Browse</span>
  </label>
  <button class="ecl-file-upload__upload" tabindex="0" type="submit">
    Upload
  </button>
  <input class="ecl-file-upload__input" name="example-input-name-3" type="file" id="example-input-id-3" multiple>
</div>

<!-- File upload with error -->
<div class="ecl-file-upload ecl-file-upload--has-error">
  <div class="ecl-file-upload__value" tabindex="0">No file selected.</div>
  <label for="example-input-id-4">
    <span class="ecl-file-upload__browse" role="button" aria-controls="example-input-id-4" tabindex="0">Browse</span>
  </label>
  <button class="ecl-file-upload__upload" tabindex="0" type="submit">
    Upload
  </button>
  <input class="ecl-file-upload__input" name="example-input-name-4" type="file" id="example-input-id-4">
</div>

  • Content:
    /**
     * File uploads related behaviors.
     */
    
    import { queryAll } from '@ec-europa/ecl-base/helpers/dom';
    
    /**
     * @param {object} options Object containing configuration overrides
     */
    export const fileUploads = ({
      selector: selector = '.ecl-file-upload',
      inputSelector: inputSelector = '.ecl-file-upload__input',
      valueSelector: valueSelector = '.ecl-file-upload__value',
      browseSelector: browseSelector = '.ecl-file-upload__browse',
    } = {}) => {
      // SUPPORTS
      if (
        !('querySelector' in document) ||
        !('addEventListener' in window) ||
        !document.documentElement.classList
      )
        return null;
    
      // SETUP
      // set file upload element NodeLists
      const fileUploadContainers = queryAll(selector);
    
      // ACTIONS
      function updateFileName(element, files) {
        if (files.length === 0) return;
    
        let filename = '';
    
        for (let i = 0; i < files.length; i += 1) {
          const file = files[i];
          if ('name' in file) {
            if (i > 0) {
              filename += ', ';
            }
            filename += file.name;
          }
        }
    
        // Show the selected filename in the field.
        const messageElement = element;
        messageElement.innerHTML = filename;
      }
    
      // EVENTS
      function eventValueChange(e) {
        if ('files' in e.target) {
          const fileUploadElements = queryAll(valueSelector, e.target.parentNode);
    
          fileUploadElements.forEach(fileUploadElement => {
            updateFileName(fileUploadElement, e.target.files);
          });
        }
      }
    
      function eventBrowseKeydown(e) {
        // collect header targets, and their prev/next
        const isModifierKey = e.metaKey || e.altKey;
    
        const inputElements = queryAll(inputSelector, e.target.parentNode);
    
        inputElements.forEach(inputElement => {
          // don't catch key events when ⌘ or Alt modifier is present
          if (isModifierKey) return;
    
          // catch enter/space, left/right and up/down arrow key events
          // if new panel show it, if next/prev move focus
          switch (e.keyCode) {
            case 13:
            case 32:
              e.preventDefault();
              inputElement.click();
              break;
            default:
              break;
          }
        });
      }
    
      // BIND EVENTS
      function bindFileUploadEvents(fileUploadContainer) {
        // bind all file upload change events
        const fileUploadInputs = queryAll(inputSelector, fileUploadContainer);
        fileUploadInputs.forEach(fileUploadInput => {
          fileUploadInput.addEventListener('change', eventValueChange);
        });
    
        // bind all file upload keydown events
        const fileUploadBrowses = queryAll(browseSelector, fileUploadContainer);
        fileUploadBrowses.forEach(fileUploadBrowse => {
          fileUploadBrowse.addEventListener('keydown', eventBrowseKeydown);
        });
      }
    
      // UNBIND EVENTS
      function unbindFileUploadEvents(fileUploadContainer) {
        const fileUploadInputs = queryAll(inputSelector, fileUploadContainer);
        // unbind all file upload change events
        fileUploadInputs.forEach(fileUploadInput => {
          fileUploadInput.removeEventListener('change', eventValueChange);
        });
    
        const fileUploadBrowses = queryAll(browseSelector, fileUploadContainer);
        // bind all file upload keydown events
        fileUploadBrowses.forEach(fileUploadBrowse => {
          fileUploadBrowse.removeEventListener('keydown', eventBrowseKeydown);
        });
      }
    
      // DESTROY
      function destroy() {
        fileUploadContainers.forEach(fileUploadContainer => {
          unbindFileUploadEvents(fileUploadContainer);
        });
      }
    
      // INIT
      function init() {
        if (fileUploadContainers.length) {
          fileUploadContainers.forEach(fileUploadContainer => {
            bindFileUploadEvents(fileUploadContainer);
          });
        }
      }
    
      init();
    
      // REVEAL API
      return {
        init,
        destroy,
      };
    };
    
    // module exports
    export default fileUploads;
    
  • URL: /components/raw/ecl-forms-file-uploads/ecl-forms-file-uploads.js
  • Filesystem Path: framework/components/ecl-forms/ecl-forms-file-uploads/ecl-forms-file-uploads.js
  • Size: 3.8 KB
  • Content:
    /*
     * File upload
     * @define file-upload
     */
    
    .ecl-file-upload {
      display: inline-flex;
      margin: 0;
      width: 100%;
    }
    
    .ecl-file-upload__value {
      background-color: #fff;
      background-image: none;
      border: 1px solid $ecl-color-shade;
      color: $ecl-color-shade;
      display: block;
      flex-grow: 1;
      font-family: $ecl-font-family-sans-serif;
      font-size: map-get($ecl-font-size, 's');
      line-height: 1.6;
      margin: 0;
      overflow: hidden;
      padding: map-get($ecl-spacing, 'xxxs') map-get($ecl-spacing, 'xxs');
      text-overflow: ellipsis;
      white-space: nowrap;
    
      &:focus {
        border-color: map-get($ecl-colors, 'yellow-110');
        outline: 3px solid map-get($ecl-colors, 'yellow-110');
        outline-offset: 0;
        text-decoration: none;
      }
    }
    
    .ecl-file-upload__browse {
      background-color: $ecl-color-shade;
      border: 2px solid transparent;
      color: #fff;
      display: inline-block;
      font-family: $ecl-font-family-sans-serif;
      font-size: map-get($ecl-font-size, 's');
      font-weight: 600;
      line-height: 1.6;
      margin: 0;
      padding: map-get($ecl-spacing, 'xxxs') map-get($ecl-spacing, 'xs');
    
      &:hover,
      &:focus,
      &:active {
        background-color: $ecl-color-primary;
        outline: 3px solid map-get($ecl-colors, 'yellow-110');
        outline-offset: -3px;
      }
    }
    
    .ecl-file-upload__upload {
      background-color: $ecl-color-primary;
      border: 2px solid transparent;
      color: #fff;
      display: inline-block;
      font-family: $ecl-font-family-sans-serif;
      font-size: map-get($ecl-font-size, 's');
      font-weight: 600;
      line-height: 1.6;
      margin-left: map-get($ecl-spacing, 'xxxs');
      padding: map-get($ecl-spacing, 'xxxs') map-get($ecl-spacing, 'xs');
    
      &:hover,
      &:focus,
      &:active {
        background-color: #fff;
        border-color: $ecl-color-primary;
        color: $ecl-color-primary;
        text-decoration: underline;
      }
    
      &:focus {
        outline: 3px solid map-get($ecl-colors, 'yellow-110');
        outline-offset: -3px;
      }
    }
    
    .ecl-file-upload__input {
      display: none;
    }
    
    // disabled
    .ecl-file-upload--is-disabled {
      .ecl-file-upload__value {
        background-color: #eee;
        cursor: not-allowed;
      }
    
      .ecl-file-upload__browse {
        cursor: not-allowed;
      }
    }
    
    // error
    .ecl-file-upload--has-error {
      .ecl-file-upload__value {
        border-color: $ecl-color-error;
        border-width: 2px;
      }
    
      .ecl-file-upload__browse {
        background-color: $ecl-color-error;
      }
    }
    
  • URL: /components/raw/ecl-forms-file-uploads/ecl-forms-file-uploads.scss
  • Filesystem Path: framework/components/ecl-forms/ecl-forms-file-uploads/ecl-forms-file-uploads.scss
  • Size: 2.4 KB
  • Handle: @ec-europa/ecl-forms-file-uploads
  • Tags: atom
  • Preview:
  • Filesystem Path: framework/components/ecl-forms/ecl-forms-file-uploads/ecl-forms-file-uploads.twig