fix: file picker and drop zone on ingest page

- Use <label for="..."> with form.image.vars.id instead of .click() on
  hidden input — display:none blocks programmatic click in some browsers
- Add drag-and-drop to the search photo drop zone (dragover/drop)
- Make extra photos input opacity:0/absolute so label trigger works too
- Camera fallback references correct searchInput variable via closure

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Simon Kuehn 2026-05-18 09:58:36 +00:00
parent 6241398390
commit 0453d0542c

View file

@ -78,6 +78,7 @@
</div>
{# ── Search photo (mandatory) ─────────────────────────────── #}
{% set searchInputId = form.image.vars.id %}
<div class="card mb-3 border-primary">
<div class="card-header bg-primary bg-opacity-10">
<h5 class="mb-0 text-primary">
@ -87,34 +88,39 @@
<div class="text-muted small mt-1">Typenschild / Aufkleber mit Modell &amp; Seriennummer</div>
</div>
<div class="card-body">
{# Hidden actual file input #}
{{ form_widget(form.image, {'attr': {'class': 'd-none', 'id': 'search-photo-input'}}) }}
{# File input — opacity:0 so label-click still works in all browsers #}
{{ form_widget(form.image, {'attr': {'style': 'position:absolute;opacity:0;width:0;height:0;', 'tabindex': '-1', 'id': searchInputId}}) }}
{{ form_errors(form.image) }}
{# Preview #}
<div id="search-photo-preview" class="mb-3 text-center" style="display:none;">
{# Drop zone / placeholder (also a drop target) #}
<div id="search-drop-zone" class="border rounded text-center p-4 text-muted bg-light mb-3"
style="cursor:pointer;transition:border-color .15s,background .15s;"
data-input="{{ searchInputId }}">
{# Preview (hidden until photo selected) #}
<div id="search-photo-preview" style="display:none;">
<img id="search-photo-img" src="" alt="Vorschau"
class="img-fluid rounded border" style="max-height:220px;">
class="img-fluid rounded" style="max-height:220px;">
<div class="mt-2">
<button type="button" class="btn btn-sm btn-outline-secondary" id="search-photo-clear">
<i class="fa fa-times me-1"></i>Entfernen
</button>
</div>
</div>
{# Placeholder when empty #}
<div id="search-photo-placeholder" class="border rounded text-center p-4 text-muted bg-light">
{# Empty state #}
<div id="search-photo-placeholder">
<i class="fa fa-image fa-3x mb-3 d-block"></i>
Noch kein Foto ausgewählt
<span>Foto hier ablegen oder Schaltfläche nutzen</span>
</div>
</div>
<div class="d-flex gap-2 mt-3">
<div class="d-flex gap-2">
<button type="button" class="btn btn-primary flex-fill" id="btn-search-camera">
<i class="fa fa-camera me-2"></i>Kamera
</button>
<button type="button" class="btn btn-outline-secondary flex-fill" id="btn-search-file">
{# label triggers the file input directly — no JS needed #}
<label for="{{ searchInputId }}" class="btn btn-outline-secondary flex-fill mb-0" style="cursor:pointer;">
<i class="fa fa-folder-open me-2"></i>Datei wählen
</button>
</label>
</div>
</div>
</div>
@ -125,9 +131,10 @@
<h5 class="mb-0"><i class="fa fa-images me-2"></i>Weitere Fotos <span class="text-muted fw-normal fs-6">(optional)</span></h5>
</div>
<div class="card-body">
{# Hidden input that holds extra files #}
{# Plain HTML input — label trigger works without JS tricks #}
<input type="file" name="additional_photos[]" id="extra-photos-input"
accept="image/jpeg,image/png,image/webp" multiple class="d-none">
accept="image/jpeg,image/png,image/webp" multiple
style="position:absolute;opacity:0;width:0;height:0;">
<div id="extra-photos-grid" class="d-flex flex-wrap gap-2 mb-3" style="min-height:60px;">
<div id="extra-photos-empty" class="text-muted small fst-italic d-flex align-items-center">
@ -139,9 +146,9 @@
<button type="button" class="btn btn-outline-primary flex-fill" id="btn-extra-camera">
<i class="fa fa-camera me-2"></i>Kamera
</button>
<button type="button" class="btn btn-outline-secondary flex-fill" id="btn-extra-file">
<label for="extra-photos-input" class="btn btn-outline-secondary flex-fill mb-0" style="cursor:pointer;">
<i class="fa fa-folder-open me-2"></i>Datei wählen
</button>
</label>
</div>
</div>
</div>
@ -243,7 +250,7 @@
errDiv.style.display = 'block';
// Mobile fallback: trigger native camera input
if (cameraTarget === 'search') {
document.getElementById('search-photo-input').click();
searchInput?.click();
} else {
document.getElementById('extra-photos-input').click();
}
@ -285,14 +292,16 @@
});
/* ── Search photo ──────────────────────────────────────────── */
const searchInput = document.getElementById('search-photo-input');
const dropZone = document.getElementById('search-drop-zone');
const searchInput = document.getElementById(dropZone.dataset.input);
const searchPreview = document.getElementById('search-photo-preview');
const searchImg = document.getElementById('search-photo-img');
const searchHolder = document.getElementById('search-photo-placeholder');
document.getElementById('btn-search-camera').addEventListener('click', () => openCamera('search'));
document.getElementById('btn-search-file').addEventListener('click', () => searchInput.click());
document.getElementById('search-photo-clear').addEventListener('click', () => {
document.getElementById('search-photo-clear').addEventListener('click', e => {
e.stopPropagation();
searchInput.value = '';
searchImg.src = '';
searchPreview.style.display = 'none';
@ -303,6 +312,24 @@
if (searchInput.files[0]) setSearchPhoto(searchInput.files[0]);
});
// Drag & drop on the drop zone
dropZone.addEventListener('dragover', e => {
e.preventDefault();
dropZone.style.borderColor = '#0d6efd';
dropZone.style.background = '#e8f0fe';
});
dropZone.addEventListener('dragleave', () => {
dropZone.style.borderColor = '';
dropZone.style.background = '';
});
dropZone.addEventListener('drop', e => {
e.preventDefault();
dropZone.style.borderColor = '';
dropZone.style.background = '';
const file = e.dataTransfer.files[0];
if (file) setSearchPhoto(file);
});
function setSearchPhoto(file) {
const dt = new DataTransfer();
dt.items.add(file);
@ -310,6 +337,8 @@
searchImg.src = URL.createObjectURL(file);
searchPreview.style.display = 'block';
searchHolder.style.display = 'none';
dropZone.style.borderColor = '';
dropZone.style.background = '';
}
/* ── Extra photos ──────────────────────────────────────────── */