Preloader
Auf dieser Website werden Daten wie z.B. Cookies gespeichert, um wichtige Funktionen der Website, einschließlich Analysen, Marketingfunktionen und Personalisierung zu ermöglichen. Sie können Ihre Einstellungen jederzeit ändern oder die Standardeinstellungen akzeptieren.
Cookie Hinweise
Datenschutzregelung
14.01.2025

Multi-Step-Kontaktformular


Voraussetzungen

  • Integration der jQuery-Bibliothek und des Smart Wizard Plugins.

1. Smart Wizard Plugin einbinden

Schritte:

  1. Lade die Smart Wizard-Plugin-Dateien (CSS und JS) in dein Projekt.
--> https://cdnjs.cloudflare.com/ajax/libs/jquery-smartwizard/4.5.1/css/smart_wizard.min.css

jquery-3.6.0.min.js (sollte auch mit 2.2.4 gehen)

--> https://cdnjs.cloudflare.com/ajax/libs/jquery-smartwizard/4.5.1/js/jquery.smartWizard.min.js

2. Formularstruktur erstellen

HTML-Struktur

  1. Gehe in den Bereich „Plugins“ und erstelle ein neues Plugin.
  2. Beispiel Code:
<form id="myForm" method="post" class="needs-validation" novalidate>
    <div id="smartwizard">
        <ul class="nav nav-progress">
            <li class="nav-item">
                <a class="nav-link" href="#step-1">
                    <div class="num">1</div>
                    Fahrzeugdaten
                </a>
            </li>
            <li class="nav-item">
                <a class="nav-link" href="#step-6">
                    <div class="num">2</div>
                    Preisangabe & Dokumente
                </a>
            </li>
            <li class="nav-item">
                <a class="nav-link" href="#step-7">
                    <div class="num">3</div>
                    Paketbuchung & Kontaktdaten
                </a>
            </li>
        </ul>

        <div class="tab-content">
            <!-- Schritt 1: Fahrzeugdaten -->
            <div id="step-1" class="tab-pane" role="tabpanel">
                <h3>Fahrzeugdaten</h3>
                <div class="form-group">
                    <label for="vin">Fahrgestellnummer: *</label>
                    <input type="text" id="vin" name="vin" class="form-control" required>
                </div>
                <div class="form-group">
                    <label for="manufacturer">Hersteller: *</label>
                    <input type="text" id="manufacturer" name="manufacturer" class="form-control" required>
                </div>
                <div class="form-group">
                    <label for="model">Modell: *</label>
                    <input type="text" id="model" name="model" class="form-control" required>
                </div>
                <div class="form-group">
                    <label for="type">Typ: *</label>
                    <select id="type" name="type" class="form-control" required>
                        <option value="">Bitte wählen</option>
                        <option value="Limousine">Limousine</option>
                        <option value="Kombi">Kombi</option>
                        <option value="Coupe">Coupe</option>
                        <option value="SUV">SUV</option>
                        <option value="Cabrio">Cabrio</option>
                        <option value="Transporter">Transporter</option>
                    </select>
                </div>
            </div>

            <!-- Schritt 2: PREISANGABE & BILDER & DOKUMENTE -->
            <div id="step-2" class="tab-pane" role="tabpanel">
                <h3>Preisangabe & Bilder & Dokumente</h3>
                <div class="form-group">
                    <label for="wish_price">Zu erzielender Wunschpreis: *</label>
                    <input type="number" id="wish_price" name="wish_price" class="form-control" required>
                </div>
                <h4>Fotos Upload:</h4>
                <div class="form-group">
                    <label>Außenansicht:</label>
                    <input type="file" name="photos_outside[]" multiple class="form-control">
                </div>
               
                <h4>Dokumenten Upload:</h4>
                <div class="form-group">
                    <label>Zulassungsbescheinigung Teil I (Fahrzeugschein):</label>
                    <input type="file" name="doc_fz_schein[]" multiple class="form-control">
                </div>
                <div class="form-group form-check">
                    <input type="checkbox" class="form-check-input" id="honesty_confirm" name="honesty_confirm" required>
                    <label class="form-check-label" for="honesty_confirm">Hiermit versichere ich, dass alle Angaben vollkommen ehrlich und wahrheitsgetreu sind.</label>
                </div>
            </div>

            <!-- Schritt 3: KONTAKTDATEN -->
            <div id="step-3" class="tab-pane" role="tabpanel">
                <h3>Kontaktdaten</h3>
                <div class="form-group">
                    <label for="firstname">Vorname: *</label>
                    <input type="text" id="firstname" name="firstname" class="form-control" required>
                </div>
                <div class="form-group">
                    <label for="lastname">Nachname: *</label>
                    <input type="text" id="lastname" name="lastname" class="form-control" required>
                </div>
                <div class="form-group">
                    <label for="email">E-Mail Adresse: *</label>
                    <input type="email" id="email" name="email" class="form-control" required>
                </div>
                <div class="form-group">
                    <label for="phone">Telefonnummer:</label>
                    <input type="text" id="phone" name="phone" class="form-control">
                </div>
                <div class="form-group">
                    <label for="address">Adresse:</label>
                    <input type="text" id="address" name="address" class="form-control">
                </div>
                <div class="form-group">
                    <label for="notes">Anmerkungen:</label>
                    <textarea id="notes" name="notes" class="form-control"></textarea>
                </div>

                <div class="form-group form-check">
                    <input type="checkbox" class="form-check-input" id="privacy_confirm" name="privacy_confirm" required>
                    <label class="form-check-label" for="privacy_confirm">
                        Hiermit erkläre ich mich mit der Verarbeitung der Daten gemäß der Datenschutzerklärung einverstanden.
                    </label>
                </div>
            </div>

            <!-- Progressbar -->
            <div class="progress">
                <div class="progress-bar" role="progressbar" style="width: 0%" aria-valuenow="0" aria-valuemin="0"
                     aria-valuemax="100"></div>
            </div>
        </div>
    </div>
    </form>

3. JavaScript zur Initialisierung

Code einfügen

  1. Füge den folgenden JavaScript-Code hinzu:
$(document).ready(function() {
  $('#smartwizard').smartWizard({
    selected: 0,
    theme: 'arrows',
    justified: true,
    autoAdjustHeight: true,
    backButtonSupport: true,
    enableUrlHash: false,
    transition: {
        animation: 'fade',
        speed: '400'
    },
    toolbar: {
        position: 'bottom', 
        showNextButton: true,
        showPreviousButton: true,
        extraHtml: ''
    },
    anchor: {
        enableNavigation: true,
        enableDoneState: true,
        markPreviousStepsAsDone: true,
        unDoneOnBackNavigation: false,
        enableDoneStateNavigation: true
    },
    keyboard: {
        keyNavigation: true,
        keyLeft: [37],
        keyRight: [39]
    },
    lang: {
        next: 'Weiter',
        previous: 'Zurück'
    }
  });

 // Nur sichtbare Felder des aktuellen Schrittes validieren
 $("#smartwizard").on("leaveStep", function(e, anchorObject, currentStepIndex, nextStepIndex, stepDirection) {
  if (stepDirection === 'forward') {
      var currentStep = $("#step-" + (currentStepIndex + 1));
      // Alle required Felder im aktuellen Step selektieren, die sichtbar sind
      var requiredFields = currentStep.find("input[required], select[required], textarea[required]").filter(":visible");

      var valid = true;
      requiredFields.each(function() {
          // Browser-Validity check
          if (!this.checkValidity()) {
              valid = false;
              // Klasse für ungültige Felder hinzufügen, um Styling/Feedback zu zeigen
              $(this).addClass('is-invalid');
          } else {
              $(this).removeClass('is-invalid');
          }
      });

      if(!valid) {
          // Ungültige Felder gefunden: Schritt nicht verlassen
          return false;
      }
  }
});


  // Auf letztem Schritt den Weiter-Button in Submit-Button umwandeln
  $("#smartwizard").on("showStep", function(e, anchorObject, stepIndex, stepDirection, stepPosition) {
    var totalSteps = $('#smartwizard').smartWizard("getStepInfo").totalSteps;
    if(stepIndex === (totalSteps - 1)) {
       // letzter Schritt
       $(".sw-btn-next").text("Absenden");
       $(".sw-btn-next").attr("type","submit");
    } else {
       $(".sw-btn-next").text("Weiter");
       $(".sw-btn-next").attr("type","button");
    }
  });

});

4. Formular im CMS einbinden

  1. Gehe in die Seitenstruktur und wähle die gewünschte Seite aus.
  2. Füge das Formular-Element an der gewünschten Position in das Template ein: plugin:dein_kontaktformular
     

5. Styling und Anpassung

  • Passe die CSS-Dateien des Smart Wizards an, um das Design auf dein Webseitenlayout abzustimmen.
/* ANCHOR Smart Wizards Formular */ 
:root {
  --sw-border-color:  #333333;
  --sw-toolbar-btn-color:  #ffffff;
  --sw-toolbar-btn-background-color:  #444444;
  --sw-anchor-default-primary-color:  #444444;
  --sw-anchor-default-secondary-color:  #cccccc;
  --sw-anchor-active-primary-color:  #888888;
  --sw-anchor-active-secondary-color:  #ffffff;
  --sw-anchor-done-primary-color:  #555555;
  --sw-anchor-done-secondary-color:  #ffffff;
  --sw-anchor-disabled-primary-color:  #333333;
  --sw-anchor-disabled-secondary-color:  #666666;
  --sw-anchor-error-primary-color:  #dc3545;
  --sw-anchor-error-secondary-color:  #ffffff;
  --sw-anchor-warning-primary-color:  #ffc107;
  --sw-anchor-warning-secondary-color:  #ffffff;
  --sw-progress-color:  #888888;
  --sw-progress-background-color:  #333333;
  --sw-loader-color:  #888888;
  --sw-loader-background-color:  #333333;
  --sw-loader-background-wrapper-color:  rgba(0,0,0,0.7);
}

#smartwizard {
  background-color: #222222;
  color: #ffffff;
  min-height: 720px;
  max-width: 90%;
  margin: auto;
}

.form-control {
  background-color: #333333;
  color: #ffffff;
  border: 1px solid #555555;
}

.form-control:focus {
  background-color: #333333;
  color: #ffffff;
  border-color: #888888;
}

label, .form-check-label {
  color: #ffffff;
}

.nav.nav-progress .nav-link {
  color: #cccccc;
}

.nav.nav-progress .nav-link.active {
  background-color: #444444;
  color: #ffffff;
}

.sw-btn-group .btn {
  background-color: #444444;
  color: #ffffff;
  border: none;
}

.sw-btn-group .btn:hover {
  background-color: #555555;
}

.progress-bar {
  background-color: #888888;
}

6. Sonstiges

  1. Speichere und teste das Formular auf der Website.
  2. Weiter Informationen findest du in der Dokumentation von Smart Wizard: http://techlaboratory.net/jquery-smartwizard

7. DateiUploadJS Plugin

  • Dieses Plugin durchsucht die Seite nach einem Formular mit Datei-Upload, anschließend passt es den Datei-Upload gemäß den in der Konfiguration festgelegten Optionen an.
  • Der Style muss mit CSS selbst ans Template angepasst werden.
/**
 * DateiUploadJS Plugin v0.1
 * 
 * Autor: Ottokar 
 * 
 * Keine Garantie benutzung auf eigene Gefahr.
 * 
 * Es kommt noch eine neue Version mit einigen verbesserungen.
 * 
 * Dieses Plugin durchsucht die Seite nach einem Formular mit:
 *   method="post" enctype="multipart/form-data"
 * und einem Input-Element mit:
 *   <input class="datei-upload-js" type="file">
 *
 * Anschließend passt es den Datei-Upload gemäß den in der Konfiguration festgelegten Optionen an.
 *
 * Konfiguration (bitte oben im Code anpassen):
 * - Anzahl der maximal hochladbaren Dateien (maxFiles)
 * - Maximale Gesamtgröße des Uploads in MB (maxTotalSizeMB)
 * - Maximale Dateigröße pro Datei in MB (maxFileSizeMB)
 * - Zulässige Dateitypen (acceptedTypes) über vordefinierte Kategorien oder Custom-Listen
 * - Custom-Liste erlaubter Endungen (customIncludeExtensions)
 * - Custom-Liste ausgeschlossener Endungen (customExcludeExtensions)
 * - Dateivorschau aktivieren (previewEnabled) und Vorschauoptionen (previewOptions)
 * - Dateien entfernen erlauben (allowRemove)
 *
 * Beispielhafte Kategorien von Dateitypen:
 *  - images: ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg']
 *  - texts: ['.txt', '.csv', '.md', '.json', '.xml']
 *  - documents: ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx']
 *  - videos: ['.mp4', '.webm', '.ogg', '.mov', '.avi']
 *  - audio: ['.mp3', '.wav', '.ogg']
 *
 * Die Vorschau kann als Liste mit Dateinamen, Icons oder Vorschaubildern gestaltet werden.
 * Bei nicht-Bildern wird ein Datei-Icon (Font Awesome) angezeigt.
 */
(function (window, document) {
    var config = {
        // Anzahl der maximal hochladbaren Dateien
        maxFiles: 5,

        // Gesamte maximale Upload-Größe in MB
        maxTotalSizeMB: 50,

        // Maximale Dateigröße pro Datei in MB
        maxFileSizeMB: 10,

        // Welche Dateitypen werden akzeptiert?
        // Optionen: 'images', 'texts', 'documents', 'videos', 'audio'
        acceptedTypes: ['images', 'texts', 'documents', 'videos', 'audio'],

        // Custom-Erweiterungen erlauben
        customIncludeExtensions: [],

        // Custom-Erweiterungen explizit ausschließen
        customExcludeExtensions: [],

        // Dateivorschau aktivieren
        previewEnabled: true,

        // Vorschauoptionen
        previewOptions: {
            // Dateinamen als Liste anzeigen
            previewAsList: true,
            // Bei Bildern Vorschaubild, sonst Icon
            previewAsThumbnail: true,
            // Entfernen-Option anzeigen
            previewRemoveOption: true
        },

        // Dateien entfernen erlauben
        allowRemove: true
    };

    // Interne Variablen
    var formElement = null;
    var fileInput = null;
    var fileList = [];
    var totalSize = 0;
    var previewContainer = null;
    var errorContainer = null;

    // Vordefinierte Kategorien von Dateiendungen
    var fileCategories = {
        images: ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg'],
        texts: ['.txt', '.csv', '.md', '.json', '.xml'],
        documents: ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.odt', '.pptx'],
        videos: ['.mp4', '.webm', '.ogg', '.mov', '.avi'],
        audio: ['.mp3', '.wav', '.ogg']
    };

    // Erweiterte Icon-Logik nach Dateityp
    // Hier erfolgt eine genauere Unterscheidung anhand der Endung
    function getFileIconClass(file) {
        var ext = '.' + file.name.split('.').pop().toLowerCase();

        // PDF
        if (ext === '.pdf') {
            return 'fa-file-pdf';
        }

        // Word
        if (ext === '.doc' || ext === '.docx') {
            return 'fa-file-word';
        }

        // PowerPoint
        if (ext === '.ppt' || ext === '.pptx') {
            return 'fa-file-powerpoint';
        }

        // Excel
        if (ext === '.xls' || ext === '.xlsx') {
            return 'fa-file-excel';
        }

        // Code-Dateien (erweitern nach Bedarf)
        var codeExtensions = ['.js', '.html', '.css', '.php', '.py', '.java', '.c', '.cpp', '.ts', '.cs', '.rb'];
        if (codeExtensions.includes(ext)) {
            return 'fa-file-code';
        }

        // Archive
        var archiveExtensions = ['.zip', '.rar', '.7z', '.tar', '.gz'];
        if (archiveExtensions.includes(ext)) {
            return 'fa-file-archive';
        }

        // Images
        if (isInArray(ext, fileCategories.images)) {
            return 'fa-file-image';
        }

        // Text
        if (isInArray(ext, fileCategories.texts)) {
            return 'fa-file-alt';
        }

        // Documents - falls nicht schon erfasst
        if (isInArray(ext, fileCategories.documents)) {
            // PDF, Word, PPT, XLS wurden schon geprüft
            // Falls irgendwas anderes in documents-Kategorie ist:
            return 'fa-file';
        }

        // Video
        if (isInArray(ext, fileCategories.videos)) {
            return 'fa-file-video';
        }

        // Audio
        if (isInArray(ext, fileCategories.audio)) {
            return 'fa-file-audio';
        }

        // Default
        return 'fa-file';
    }

    document.addEventListener('DOMContentLoaded', init);

    function init() {
        // Suche nach Formular mit POST und multipart/form-data
        var forms = document.querySelectorAll('form[method="post"][enctype="multipart/form-data"]');
        if (!forms.length) return;

        forms.forEach(function (frm) {
            var input = frm.querySelector('input.datei-upload-js[type="file"]');
            if (input) {
                formElement = frm;
                fileInput = input;
                setupFileInput();
            }
        });
    }

    function setupFileInput() {
        fileInput.addEventListener('change', handleFileSelect);

        // Container für Fehlerausgabe
        errorContainer = document.createElement('div');
        errorContainer.className = 'datei-upload-js-error-container';
        errorContainer.style.color = 'red';
        errorContainer.style.marginBottom = '10px';
        errorContainer.style.display = 'none';
        fileInput.parentNode.insertBefore(errorContainer, fileInput);

        // Vorschau-Container erstellen, falls aktiviert
        if (config.previewEnabled) {
            previewContainer = document.createElement('div');
            previewContainer.className = 'datei-upload-js-preview-container';
            fileInput.parentNode.insertBefore(previewContainer, fileInput.nextSibling);
        }
    }

    // Beim Ändern der Auswahl neue Dateien hinzufügen
    function handleFileSelect(e) {
        var newFiles = Array.from(e.target.files);

        // Vor der Validierung: Prüfen auf Duplikate
        newFiles = newFiles.filter(function(f) {
            return !fileList.some(function(existing) {
                return existing.name === f.name && existing.size === f.size;
            });
        });

        var result = validateFiles(newFiles);
        var validFiles = result.validFiles;
        var errors = result.errors;

        // Validierte Dateien zur bestehenden Liste hinzufügen
        validFiles.forEach(function(f){
            fileList.push(f);
            totalSize += f.size;
        });

        // Error Meldungen anzeigen
        showErrors(errors);

        // Vorschau aktualisieren
        if (config.previewEnabled) {
            renderPreview();
        }

        // FileInput aktualisieren mit allen (alten+neuen) Dateien
        updateFileInput();
    }

    function validateFiles(files) {
        var valid = [];
        var errors = [];
        var allowedExtensions = getAllowedExtensions();
        var maxFiles = config.maxFiles;
        var maxTotalSize = config.maxTotalSizeMB * 1024 * 1024;
        var maxFileSize = config.maxFileSizeMB * 1024 * 1024;

        // Prüfe, ob maximale Anzahl (bestehend + neu) überschritten wird
        if (fileList.length + files.length > maxFiles) {
            var allowedCount = maxFiles - fileList.length;
            if (allowedCount < files.length) {
                errors.push('Es dürfen maximal ' + maxFiles + ' Dateien hochgeladen werden. Du hast bereits ' + fileList.length + ' hochgeladen. Nur ' + allowedCount + ' weitere Dateien sind erlaubt.');
                files = files.slice(0, allowedCount);
            }
        }

        var currentTotalSize = totalSize;
        for (var i = 0; i < files.length; i++) {
            var file = files[i];
            var ext = '.' + file.name.split('.').pop().toLowerCase();

            // Dateiendung prüfen
            if (!isExtensionAllowed(ext, allowedExtensions)) {
                errors.push('Die Datei "' + file.name + '" hat einen nicht erlaubten Dateityp.');
                continue;
            }

            // Größe prüfen
            if (file.size > maxFileSize) {
                errors.push('Die Datei "' + file.name + '" ist zu groß. Maximal ' + config.maxFileSizeMB + 'MB pro Datei.');
                continue;
            }

            // Gesamtgröße prüfen
            var newSize = currentTotalSize + file.size;
            if (newSize > maxTotalSize) {
                errors.push('Durch die Datei "' + file.name + '" würde die maximale Gesamtgröße von ' + config.maxTotalSizeMB + 'MB überschritten.');
                continue;
            }

            // Wenn alles passt
            valid.push(file);
            currentTotalSize = newSize;
        }

        return { validFiles: valid, errors: errors };
    }

    function getAllowedExtensions() {
        var allowed = [];
        if (config.acceptedTypes && config.acceptedTypes.length > 0) {
            config.acceptedTypes.forEach(function (typeCategory) {
                if (fileCategories[typeCategory]) {
                    allowed = allowed.concat(fileCategories[typeCategory]);
                }
            });
        }

        if (config.customIncludeExtensions.length > 0) {
            allowed = allowed.concat(config.customIncludeExtensions.map(function(e){return e.toLowerCase();}));
        }

        return allowed;
    }

    function isExtensionAllowed(ext, allowedExtensions) {
        if (config.customExcludeExtensions.map(function(e){return e.toLowerCase();}).includes(ext)) {
            return false;
        }

        if (allowedExtensions.length > 0) {
            return allowedExtensions.includes(ext.toLowerCase());
        }

        return false;
    }

    function renderPreview() {
        if (!previewContainer) return;
        previewContainer.innerHTML = '';

        var list = document.createElement('ul');
        list.className = 'datei-upload-js-preview-list';

        fileList.forEach(function (file, index) {
            var li = document.createElement('li');
            li.className = 'datei-upload-js-preview-item';

            // Dateiname
            if (config.previewOptions.previewAsList) {
                var fileName = document.createElement('span');
                fileName.className = 'datei-upload-js-preview-filename';
                fileName.textContent = file.name;
                li.appendChild(fileName);
            }

            // Thumbnail oder Icon
            if (config.previewOptions.previewAsThumbnail) {
                if (isImage(file)) {
                    var img = document.createElement('img');
                    img.className = 'datei-upload-js-preview-image';
                    img.src = URL.createObjectURL(file);
                    img.alt = file.name;
                    li.appendChild(img);
                } else {
                    var iconClass = getFileIconClass(file);
                    var icon = document.createElement('i');
                    icon.className = 'fa ' + iconClass;
                    icon.style.fontSize = '48px';
                    icon.style.marginLeft = '10px';
                    li.appendChild(icon);
                }
            }

            // Remove Option
            if (config.previewOptions.previewRemoveOption && config.allowRemove) {
                var removeBtn = document.createElement('button');
                removeBtn.type = 'button';
                removeBtn.className = 'datei-upload-js-preview-remove';
                removeBtn.textContent = 'Entfernen';
                removeBtn.style.marginLeft = '10px';
                removeBtn.addEventListener('click', function() {
                    removeFile(index);
                });
                li.appendChild(removeBtn);
            }

            list.appendChild(li);
        });

        previewContainer.appendChild(list);
    }

    function removeFile(index) {
        if (!config.allowRemove) return;
        var removedFile = fileList.splice(index, 1)[0];
        totalSize -= removedFile.size;
        updateFileInput();
        renderPreview();
    }

    function updateFileInput() {
        var dataTransfer = new DataTransfer();
        fileList.forEach(function(file) {
            dataTransfer.items.add(file);
        });
        fileInput.files = dataTransfer.files;
    }

    function isImage(file) {
        var type = file.type.toLowerCase();
        return type.startsWith('image/');
    }

    function isInArray(value, array) {
        return array.indexOf(value) !== -1;
    }

    function showErrors(errors) {
        if (!errorContainer) return;
        if (errors.length > 0) {
            errorContainer.style.display = 'block';
            errorContainer.innerHTML = '';
            var ul = document.createElement('ul');
            ul.style.paddingLeft = '20px';
            errors.forEach(function(err) {
                var li = document.createElement('li');
                li.textContent = err;
                ul.appendChild(li);
            });
            errorContainer.appendChild(ul);
        } else {
            errorContainer.style.display = 'none';
            errorContainer.innerHTML = '';
        }
    }

})(window, document);

8. MultiDateiUploadJS Plugin

  • Dieses Plugin ist eine angepasste Version, welche mehrere Uploade Felder unterstützt sowie Data-Attribute
/**
 * DateiUploadJS Plugin v1.1 (Mehrfachfelder & Data-Attribut-Konfiguration)
 *
 * Autor: Ottokar
 * 
 * Keine Garantie benutzung auf eigene Gefahr.
 * 
 * Dieses Plugin initialisiert Datei-Uploads in Formularen, die folgende Felder enthalten:
 *   <input type="file" class="datei-upload-js" ...>
 *   <input type="file" class="multi-datei-upload-js" ...>
 *
 * - Bei .datei-upload-js wird eine Standardkonfiguration (defaultConfig) verwendet.
 * - Bei .multi-datei-upload-js kann man per data-Attributen gezielt Optionen anpassen.
 *   Beispiel:
 *     <input type="file"
 *            class="multi-datei-upload-js"
 *            data-max-files="10"
 *            data-max-file-size-mb="5"
 *            data-accepted-types="images,documents"
 *            data-custom-include-extensions=".zip,.rar"
 *            data-custom-exclude-extensions=".exe,.apk"
 *            data-preview-enabled="true"
 *            data-required="true"
 *            data-min-required-files="2">
 *
 * Haupt-Funktionen:
 *  - maxFiles, maxFileSizeMB, maxTotalSizeMB
 *  - acceptedTypes (Nutzen von vordefinierten Kategorien)
 *  - customIncludeExtensions, customExcludeExtensions
 *  - Vorschau mit Thumbnails oder Icons
 *  - Dateien entfernen (falls allowRemove=true)
 *  - Required-Funktion: erfordert mind. X Dateien
 *
 * Nutzung:
 *  1. Plugin-Script einbinden
 *  2. <form method="post" enctype="multipart/form-data"> ... </form>
 *  3. Input-Felder mit .datei-upload-js oder .multi-datei-upload-js
 *  4. (Optional) Data-Attribute für .multi-datei-upload-js zum Überschreiben der Config
 *  5. Plugin startet automatisch (DOM Loaded), durchsucht Formulare & Inputs
 */

(function (window, document) {

    // Standard-/Fallback-Konfiguration
    var defaultConfig = {
      maxFiles: 25,
      maxTotalSizeMB: 100,
      maxFileSizeMB: 10,
      acceptedTypes: ['images', 'texts', 'documents', 'videos', 'audio'],
      customIncludeExtensions: [],
      customExcludeExtensions: [],
      previewEnabled: true,
      allowRemove: true,
      previewOptions: {
        previewAsList: true,
        previewAsThumbnail: true,
        previewRemoveOption: true
      },
      // => Required-Funktion
      required: false,            // Der Nutzer muss hier zwingend etwas hochladen
      minRequiredFiles: 1         // Falls required=true, wie viele Datein mindestens
    };
  
    // Vordefinierte Kategorien von Dateiendungen
    var fileCategories = {
      images: ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg'],
      texts: ['.txt', '.csv', '.md', '.json', '.xml'],
      documents: ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.odt', '.pptx'],
      videos: ['.mp4', '.webm', '.ogg', '.mov', '.avi'],
      audio: ['.mp3', '.wav', '.ogg']
    };
  
    // Hauptfunktion: Start bei DOMContentLoaded
    document.addEventListener('DOMContentLoaded', initAllUploads);
  
    function initAllUploads() {
      // Suche alle Formulare mit method="post" und enctype="multipart/form-data"
      var forms = document.querySelectorAll('form[method="post"][enctype="multipart/form-data"]');
      if (!forms.length) return;
  
      forms.forEach(function(form) {
        // Jede Instanz von .datei-upload-js und .multi-datei-upload-js verarbeiten
        var uploadInputs = form.querySelectorAll('input[type="file"].datei-upload-js, input[type="file"].multi-datei-upload-js');
        if (!uploadInputs.length) return;
  
        uploadInputs.forEach(function(fileInput) {
          // Ermitteln, ob multi-Variante
          var isMulti = fileInput.classList.contains('multi-datei-upload-js');
  
          // Konfiguration ermitteln (Standard + ggf. Data-Attribute)
          var config = buildConfig(fileInput, isMulti);
  
          // Instanz-Objekt erzeugen
          createUploadInstance(form, fileInput, config);
        });
      });
    }
  
    /**
     * Liest ggf. vorhandene data-Attribute vom <input> aus (nur bei multi-datei-upload-js).
     * Gibt ein Config-Objekt zurück (Merge aus defaultConfig + überschriebenen Werten).
     */
    function buildConfig(fileInput, isMulti) {
      // Kopie der defaultConfig erzeugen
      var config = JSON.parse(JSON.stringify(defaultConfig));
  
      // Nur .multi-datei-upload-js nutzt data-Attribute
      if (!isMulti) {
        return config;
      }
  
      // Lese mögliche data-Attribute
      // (alle parseInt o. parseFloat oder strings -> arrays)
      if (fileInput.hasAttribute('data-max-files')) {
        config.maxFiles = parseInt(fileInput.getAttribute('data-max-files'), 10);
      }
      if (fileInput.hasAttribute('data-max-total-size-mb')) {
        config.maxTotalSizeMB = parseInt(fileInput.getAttribute('data-max-total-size-mb'), 10);
      }
      if (fileInput.hasAttribute('data-max-file-size-mb')) {
        config.maxFileSizeMB = parseInt(fileInput.getAttribute('data-max-file-size-mb'), 10);
      }
      if (fileInput.hasAttribute('data-accepted-types')) {
        // Kommagetrennte Liste -> Array
        var str = fileInput.getAttribute('data-accepted-types');
        var arr = str.split(',').map(function(item){return item.trim();});
        config.acceptedTypes = arr;
      }
      if (fileInput.hasAttribute('data-custom-include-extensions')) {
        // Kommagetrennt
        var incStr = fileInput.getAttribute('data-custom-include-extensions');
        config.customIncludeExtensions = incStr.split(',').map(function(item){return item.trim().toLowerCase();});
      }
      if (fileInput.hasAttribute('data-custom-exclude-extensions')) {
        // Kommagetrennt
        var excStr = fileInput.getAttribute('data-custom-exclude-extensions');
        config.customExcludeExtensions = excStr.split(',').map(function(item){return item.trim().toLowerCase();});
      }
      if (fileInput.hasAttribute('data-preview-enabled')) {
        var prev = fileInput.getAttribute('data-preview-enabled');
        config.previewEnabled = (prev === 'true');
      }
      if (fileInput.hasAttribute('data-allow-remove')) {
        var rm = fileInput.getAttribute('data-allow-remove');
        config.allowRemove = (rm === 'true');
      }
      // PreviewOptions - bei Bedarf hier erweitern
      // data-preview-as-list, data-preview-as-thumbnail, data-preview-remove-option etc.
      // oder man liest einfach alle raus, wenn man das möchte.
  
      // Required-Funktion
      if (fileInput.hasAttribute('data-required')) {
        var req = fileInput.getAttribute('data-required');
        config.required = (req === 'true');
      }
      if (fileInput.hasAttribute('data-min-required-files')) {
        config.minRequiredFiles = parseInt(fileInput.getAttribute('data-min-required-files'), 10);
      }
  
      return config;
    }
  
    /**
     * Erstellt pro Feld eine "Instanz" (eigener State).
     */
    function createUploadInstance(formElement, fileInput, config) {
      // Interne Daten für diese Instanz
      var instance = {
        formElement: formElement,
        fileInput: fileInput,
        config: config,
        fileList: [],
        totalSize: 0,
        errorContainer: null,
        previewContainer: null
      };
  
      // Setup
      setupFileInput(instance);
    }
  
    /**
     * Setzt Event Listener, erstellt DOM-Container etc.
     */
    function setupFileInput(instance) {
      var input = instance.fileInput;
  
      // Fehlermeldungs-Container
      var errCtnr = document.createElement('div');
      errCtnr.className = 'datei-upload-js-error-container';
      errCtnr.style.color = 'red';
      errCtnr.style.marginBottom = '10px';
      errCtnr.style.display = 'none';
      input.parentNode.insertBefore(errCtnr, input);
      instance.errorContainer = errCtnr;
  
      // Vorschau-Container
      if (instance.config.previewEnabled) {
        var previewCtnr = document.createElement('div');
        previewCtnr.className = 'datei-upload-js-preview-container';
        input.parentNode.insertBefore(previewCtnr, input.nextSibling);
        instance.previewContainer = previewCtnr;
      }
  
      // EventListener
      input.addEventListener('change', function(e){
        handleFileSelect(e, instance);
      });
  
      // Wenn das Formular abgeschickt wird und dieses Feld required ist, prüfen wir:
      if (instance.config.required) {
        instance.formElement.addEventListener('submit', function(e) {
          // Prüfen, ob mind. minRequiredFiles hochgeladen
          if (instance.fileList.length < instance.config.minRequiredFiles) {
            e.preventDefault();
            showErrors(instance, [
              'Bitte laden Sie mindestens ' + instance.config.minRequiredFiles + ' Datei(en) hoch.'
            ]);
            // Scroll zum Fehler
            errCtnr.scrollIntoView({ behavior: 'smooth', block: 'center' });
          }
        });
      }
    }
  
    /**
     * Wird aufgerufen, wenn der Benutzer neue Dateien auswählt.
     */
    function handleFileSelect(e, instance) {
      var newFiles = Array.from(e.target.files);
      var config = instance.config;
  
      // Duplikate filtern (Name + Größe)
      newFiles = newFiles.filter(function(f) {
        return !instance.fileList.some(function(existing) {
          return existing.name === f.name && existing.size === f.size;
        });
      });
  
      // Validierung
      var result = validateFiles(newFiles, instance);
      var validFiles = result.validFiles;
      var errors = result.errors;
  
      // Übernommene Dateien zur fileList hinzufügen
      validFiles.forEach(function(file){
        instance.fileList.push(file);
        instance.totalSize += file.size;
      });
  
      // Fehler ggf. ausgeben
      showErrors(instance, errors);
  
      // Vorschau aktualisieren
      if (config.previewEnabled) {
        renderPreview(instance);
      }
  
      // FileInput-Objekt aktualisieren
      updateFileInput(instance);
    }
  
    /**
     * Prüft Dateien gemäß config (maxFiles, maxFileSize, etc.).
     */
    function validateFiles(files, instance) {
      var valid = [];
      var errors = [];
      var config = instance.config;
  
      var allowedExtensions = getAllowedExtensions(config);
      var maxFiles = config.maxFiles;
      var maxTotalSize = config.maxTotalSizeMB * 1024 * 1024;
      var maxFileSize = config.maxFileSizeMB * 1024 * 1024;
  
      // Prüfe maximale Anzahl
      if (instance.fileList.length + files.length > maxFiles) {
        var allowedCount = maxFiles - instance.fileList.length;
        if (allowedCount < files.length) {
          errors.push('Es dürfen maximal ' + maxFiles + ' Dateien hochgeladen werden. Du hast bereits ' + instance.fileList.length + ' hochgeladen. Nur ' + allowedCount + ' weitere Datei(en) möglich.');
          // Nur so viele übernehmen, wie noch erlaubt
          files = files.slice(0, allowedCount);
        }
      }
  
      var currentTotalSize = instance.totalSize;
      files.forEach(function(file) {
        var ext = '.' + file.name.split('.').pop().toLowerCase();
  
        // Dateiendung
        if (!isExtensionAllowed(ext, allowedExtensions, config)) {
          errors.push('Die Datei "' + file.name + '" hat einen nicht erlaubten Dateityp.');
          return;
        }
  
        // Größe pro Datei
        if (file.size > maxFileSize) {
          errors.push('Die Datei "' + file.name + '" überschreitet die Maximalgröße von ' + config.maxFileSizeMB + 'MB.');
          return;
        }
  
        // Gesamtgröße
        var newSize = currentTotalSize + file.size;
        if (newSize > maxTotalSize) {
          errors.push('Durch die Datei "' + file.name + '" würde die maximale Gesamtgröße von ' + config.maxTotalSizeMB + 'MB überschritten.');
          return;
        }
  
        // alles okay
        valid.push(file);
        currentTotalSize = newSize;
      });
  
      return {
        validFiles: valid,
        errors: errors
      };
    }
  
    /**
     * Ermittelt das "Allowed Extensions"-Array anhand acceptedTypes und customIncludeExtensions.
     */
    function getAllowedExtensions(config) {
      var allowed = [];
  
      if (config.acceptedTypes && config.acceptedTypes.length > 0) {
        config.acceptedTypes.forEach(function(cat) {
          var catLower = cat.toLowerCase();
          if (fileCategories[catLower]) {
            allowed = allowed.concat(fileCategories[catLower]);
          } else {
            // Falls ein Tippfehler in data-accepted-types -> ignorieren wir es
          }
        });
      }
  
      if (config.customIncludeExtensions.length > 0) {
        // sicherstellen, dass lowerCase
        allowed = allowed.concat(config.customIncludeExtensions.map(function(e){ return e.toLowerCase(); }));
      }
  
      // Falls gar nix drin: user akzeptiert eventuell nichts => Dann kann man hier so tun,
      // dass wir "alles blockieren" oder "alles erlauben".
      // Hier: wir blockieren alles, wenn acceptedTypes und include leer sind.
      return allowed;
    }
  
    /**
     * Prüft, ob Extension erlaubt ist (unter Berücksichtigung von customExcludeExtensions).
     */
    function isExtensionAllowed(ext, allowedExtensions, config) {
      var exclude = config.customExcludeExtensions.map(function(e){ return e.toLowerCase(); });
      // Ist es explizit ausgeschlossen?
      if (exclude.includes(ext)) {
        return false;
      }
      // Wenn allowedExtensions befüllt ist, muss ext dort enthalten sein.
      // Ist allowedExtensions leer -> nichts erlaubt
      if (allowedExtensions.length > 0) {
        return allowedExtensions.includes(ext);
      }
      return false;
    }
  
    /**
     * Zeigt Vorschau (Liste der hochgeladenen Dateien, ggf. Thumbnails).
     */
    function renderPreview(instance) {
      if (!instance.previewContainer) return;
      var config = instance.config;
      var fileList = instance.fileList;
      var container = instance.previewContainer;
  
      container.innerHTML = '';
  
      var list = document.createElement('ul');
      list.className = 'datei-upload-js-preview-list';
  
      fileList.forEach(function(file, idx) {
        var li = document.createElement('li');
        li.className = 'datei-upload-js-preview-item';
  
        // Dateiname
        if (config.previewOptions.previewAsList) {
          var fileNameSpan = document.createElement('span');
          fileNameSpan.className = 'datei-upload-js-preview-filename';
          fileNameSpan.textContent = file.name;
          li.appendChild(fileNameSpan);
        }
  
        // Thumbnail oder Icon
        if (config.previewOptions.previewAsThumbnail) {
          if (isImage(file)) {
            var img = document.createElement('img');
            img.className = 'datei-upload-js-preview-image';
            img.src = URL.createObjectURL(file);
            img.alt = file.name;
            li.appendChild(img);
          } else {
            var iconClass = getFileIconClass(file);
            var icon = document.createElement('i');
            icon.className = 'fa ' + iconClass;
            icon.style.fontSize = '48px';
            icon.style.marginLeft = '10px';
            li.appendChild(icon);
          }
        }
  
        // Entfernen-Button
        if (config.previewOptions.previewRemoveOption && config.allowRemove) {
          var removeBtn = document.createElement('button');
          removeBtn.type = 'button';
          removeBtn.className = 'datei-upload-js-preview-remove';
          removeBtn.textContent = 'Entfernen';
          removeBtn.style.marginLeft = '10px';
          removeBtn.addEventListener('click', function(){
            removeFile(instance, idx);
          });
          li.appendChild(removeBtn);
        }
  
        list.appendChild(li);
      });
  
      container.appendChild(list);
    }
  
    /**
     * Entfernt eine Datei aus der Liste.
     */
    function removeFile(instance, index) {
      if (!instance.config.allowRemove) return;
      var removedFile = instance.fileList.splice(index, 1)[0];
      instance.totalSize -= removedFile.size;
      // Neu aufbauen
      updateFileInput(instance);
      renderPreview(instance);
    }
  
    /**
     * Aktualisiert das native FileInput.files-Objekt anhand instance.fileList.
     */
    function updateFileInput(instance) {
      var dataTransfer = new DataTransfer();
      instance.fileList.forEach(function(f){
        dataTransfer.items.add(f);
      });
      instance.fileInput.files = dataTransfer.files;
    }
  
    /**
     * Zeigt Fehlerliste an (oder versteckt sie, wenn none).
     */
    function showErrors(instance, errors) {
      if (!instance.errorContainer) return;
      if (errors.length > 0) {
        instance.errorContainer.style.display = 'block';
        instance.errorContainer.innerHTML = '';
        var ul = document.createElement('ul');
        ul.style.paddingLeft = '20px';
        errors.forEach(function(err){
          var li = document.createElement('li');
          li.textContent = err;
          ul.appendChild(li);
        });
        instance.errorContainer.appendChild(ul);
      } else {
        instance.errorContainer.style.display = 'none';
        instance.errorContainer.innerHTML = '';
      }
    }
  
    // ------------------------------------
    // Hilfsfunktionen für Icons & Bilder
    // ------------------------------------
    function isImage(file) {
      var type = file.type.toLowerCase();
      return type.startsWith('image/');
    }
  
    /**
     * Wählt eine Font-Awesome-Icon-Klasse anhand der Dateiendung.
     */
    function getFileIconClass(file) {
      var ext = '.' + file.name.split('.').pop().toLowerCase();
  
      // PDF
      if (ext === '.pdf') {
        return 'fa-file-pdf';
      }
      // Word
      if (ext === '.doc' || ext === '.docx') {
        return 'fa-file-word';
      }
      // PowerPoint
      if (ext === '.ppt' || ext === '.pptx') {
        return 'fa-file-powerpoint';
      }
      // Excel
      if (ext === '.xls' || ext === '.xlsx') {
        return 'fa-file-excel';
      }
      // Code
      var codeExtensions = ['.js', '.html', '.css', '.php', '.py', '.java', '.c', '.cpp', '.ts', '.cs', '.rb'];
      if (codeExtensions.includes(ext)) {
        return 'fa-file-code';
      }
      // Archive
      var archiveExtensions = ['.zip', '.rar', '.7z', '.tar', '.gz'];
      if (archiveExtensions.includes(ext)) {
        return 'fa-file-archive';
      }
      // Images
      if (fileCategories.images.includes(ext)) {
        return 'fa-file-image';
      }
      // Text
      if (fileCategories.texts.includes(ext)) {
        return 'fa-file-alt';
      }
      // Documents
      if (fileCategories.documents.includes(ext)) {
        return 'fa-file';
      }
      // Video
      if (fileCategories.videos.includes(ext)) {
        return 'fa-file-video';
      }
      // Audio
      if (fileCategories.audio.includes(ext)) {
        return 'fa-file-audio';
      }
      // Default
      return 'fa-file';
    }
  
  })(window, document);