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:
parent
6241398390
commit
0453d0542c
1 changed files with 58 additions and 29 deletions
|
|
@ -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 & 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;">
|
||||
<img id="search-photo-img" src="" alt="Vorschau"
|
||||
class="img-fluid rounded border" 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>
|
||||
{# 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" 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>
|
||||
{# Empty state #}
|
||||
<div id="search-photo-placeholder">
|
||||
<i class="fa fa-image fa-3x mb-3 d-block"></i>
|
||||
<span>Foto hier ablegen oder Schaltfläche nutzen</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Placeholder when empty #}
|
||||
<div id="search-photo-placeholder" class="border rounded text-center p-4 text-muted bg-light">
|
||||
<i class="fa fa-image fa-3x mb-3 d-block"></i>
|
||||
Noch kein Foto ausgewählt
|
||||
</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');
|
||||
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 ──────────────────────────────────────────── */
|
||||
|
|
|
|||
Loading…
Reference in a new issue