14.01.2025Multi-Step-Kontaktformular
Voraussetzungen
- Integration der jQuery-Bibliothek und des Smart Wizard Plugins.
1. Smart Wizard Plugin einbinden
Schritte:
- 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
- Gehe in den Bereich „Plugins“ und erstelle ein neues Plugin.
- 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
- 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
- Gehe in die Seitenstruktur und wähle die gewünschte Seite aus.
- 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
- Speichere und teste das Formular auf der Website.
- 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);