PWA: Dokumente (PDFs) in Mehrfachauswahl + Back-Button schließt Auswahl [deploy]
All checks were successful
Deploy baustelle-pwa / deploy (push) Successful in 4s

- PDFs/Dokumente lassen sich jetzt auch im Auswahl-Modus markieren und
  mitteilen (bisher öffnete der Tap immer den Viewer)
- ☑ Auswählen-Button erscheint auch wenn nur Dokumente vorhanden sind
- Android-Zurück-Button im Select-Modus: hebt die Auswahl auf und
  beendet den Auswahl-Modus (statt die Seite zu verlassen)
- ✕ Abbrechen-Button nutzt denselben History-Back-Pfad

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Eddy 2026-04-18 12:38:06 +02:00
parent 427fdeb0c0
commit 0a78d9e5d9
2 changed files with 73 additions and 14 deletions

24
app.css
View file

@ -352,6 +352,30 @@ body {
border-color: #fff; border-color: #fff;
} }
/* Auswahl-Mode in der Dokumenten-Liste (PDFs etc.) */
.doc-item .doc-check {
display: none;
width: 22px;
height: 22px;
border-radius: 50%;
background: transparent;
border: 2px solid #888;
color: transparent;
font-size: 14px;
font-weight: bold;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.doc-list.selecting .doc-item .doc-check { display: flex; }
.doc-list.selecting .doc-item .doc-open { pointer-events: none; opacity: 0.4; }
.doc-list.selecting .doc-item.selected { background: rgba(90,176,255,0.15); outline: 2px solid #5ab0ff; }
.doc-list.selecting .doc-item.selected .doc-check {
background: #5ab0ff;
border-color: #5ab0ff;
color: #fff;
}
/* Sticky Aktions-Bar am unteren Bildschirmrand */ /* Sticky Aktions-Bar am unteren Bildschirmrand */
.select-bar { .select-bar {
position: fixed; position: fixed;

63
app.js
View file

@ -45,6 +45,9 @@ function setBack(visible, hash) {
const modalStack = []; const modalStack = [];
let _skipNextPopstate = false; let _skipNextPopstate = false;
let _lastBackAt = 0; let _lastBackAt = 0;
// Aktiver Select-Mode-Cleanup: wird von popstate gerufen, damit Android-Back
// eine laufende Mehrfachauswahl beendet statt die Seite zu verlassen.
let selectModeCleanup = null;
function pushModal(el, cleanup) { function pushModal(el, cleanup) {
modalStack.push({ el, cleanup: cleanup || null }); modalStack.push({ el, cleanup: cleanup || null });
@ -83,6 +86,12 @@ window.addEventListener('popstate', () => {
try { top.cleanup && top.cleanup(); } catch {} try { top.cleanup && top.cleanup(); } catch {}
return; return;
} }
if (selectModeCleanup) {
const fn = selectModeCleanup;
selectModeCleanup = null;
try { fn(); } catch {}
return;
}
if (isTopLevelHash(location.hash)) { if (isTopLevelHash(location.hash)) {
const now = Date.now(); const now = Date.now();
if (now - _lastBackAt < 2000) return; if (now - _lastBackAt < 2000) return;
@ -535,7 +544,7 @@ router.on('/orders/:id', async (args) => {
<div class="detail-section" style="margin-top:16px;"> <div class="detail-section" style="margin-top:16px;">
<div class="photo-section-head"> <div class="photo-section-head">
<h3 style="margin:0;">Hochgeladene Fotos (${imagePhotos.length})</h3> <h3 style="margin:0;">Hochgeladene Fotos (${imagePhotos.length})</h3>
${imagePhotos.length ? '<button class="btn btn-small" id="btn-select-mode" title="Mehrfachauswahl">☑ Auswählen</button>' : ''} ${(imagePhotos.length + otherDocs.length) ? '<button class="btn btn-small" id="btn-select-mode" title="Mehrfachauswahl">☑ Auswählen</button>' : ''}
</div> </div>
<div class="photo-grid" id="photo-grid"> <div class="photo-grid" id="photo-grid">
${imagePhotos.map(p => `<div class="thumb" data-relpath="${escapeHtml(p.relpath)}"><div class="thumb-placeholder">⏳</div><div class="thumb-check">✓</div></div>`).join('')} ${imagePhotos.map(p => `<div class="thumb" data-relpath="${escapeHtml(p.relpath)}"><div class="thumb-placeholder">⏳</div><div class="thumb-check">✓</div></div>`).join('')}
@ -558,6 +567,7 @@ router.on('/orders/:id', async (args) => {
<h3>Weitere Dokumente (${otherDocs.length})</h3> <h3>Weitere Dokumente (${otherDocs.length})</h3>
<div class="doc-list"> <div class="doc-list">
${otherDocs.map(p => `<div class="doc-item" data-relpath="${escapeHtml(p.relpath)}" data-mime="${escapeHtml(p.mime || '')}" data-filename="${escapeHtml(p.filename)}"> ${otherDocs.map(p => `<div class="doc-item" data-relpath="${escapeHtml(p.relpath)}" data-mime="${escapeHtml(p.mime || '')}" data-filename="${escapeHtml(p.filename)}">
<div class="doc-check"></div>
<span class="doc-icon">${docIconFor(p.filename, p.mime)}</span> <span class="doc-icon">${docIconFor(p.filename, p.mime)}</span>
<div class="doc-meta"> <div class="doc-meta">
<div class="doc-name">${escapeHtml(p.filename)}</div> <div class="doc-name">${escapeHtml(p.filename)}</div>
@ -571,6 +581,7 @@ router.on('/orders/:id', async (args) => {
// Foto-Thumbnails: Tap = Vollbild-Modal (oder Auswahl im Select-Modus) // Foto-Thumbnails: Tap = Vollbild-Modal (oder Auswahl im Select-Modus)
const photoGrid = document.getElementById('photo-grid'); const photoGrid = document.getElementById('photo-grid');
const docList = document.querySelector('.doc-list');
photoGrid.querySelectorAll('.thumb').forEach(t => { photoGrid.querySelectorAll('.thumb').forEach(t => {
t.addEventListener('click', () => { t.addEventListener('click', () => {
if (photoGrid.classList.contains('selecting')) { if (photoGrid.classList.contains('selecting')) {
@ -586,17 +597,33 @@ router.on('/orders/:id', async (args) => {
const selectBtn = document.getElementById('btn-select-mode'); const selectBtn = document.getElementById('btn-select-mode');
if (selectBtn) selectBtn.onclick = () => startSelectMode(); if (selectBtn) selectBtn.onclick = () => startSelectMode();
function allSelectables() {
const thumbs = Array.from(photoGrid.querySelectorAll('.thumb[data-relpath]'));
const docs = docList ? Array.from(docList.querySelectorAll('.doc-item[data-relpath]')) : [];
return thumbs.concat(docs);
}
function startSelectMode() { function startSelectMode() {
if (photoGrid.classList.contains('selecting')) return;
photoGrid.classList.add('selecting'); photoGrid.classList.add('selecting');
if (docList) docList.classList.add('selecting');
renderSelectBar(); renderSelectBar();
updateSelectBar(); updateSelectBar();
// Android-Back soll nur die Auswahl aufheben
history.pushState({ _selecting: true }, '', location.hash);
selectModeCleanup = endSelectMode;
} }
function endSelectMode() { function endSelectMode() {
selectModeCleanup = null;
photoGrid.classList.remove('selecting'); photoGrid.classList.remove('selecting');
photoGrid.querySelectorAll('.thumb.selected').forEach(x => x.classList.remove('selected')); if (docList) docList.classList.remove('selecting');
allSelectables().forEach(x => x.classList.remove('selected'));
const bar = document.getElementById('select-bar'); const bar = document.getElementById('select-bar');
if (bar) bar.remove(); if (bar) bar.remove();
} }
function cancelViaBack() {
if (history.state && history.state._selecting) history.back();
else endSelectMode();
}
function renderSelectBar() { function renderSelectBar() {
if (document.getElementById('select-bar')) return; if (document.getElementById('select-bar')) return;
const bar = document.createElement('div'); const bar = document.createElement('div');
@ -609,42 +636,50 @@ router.on('/orders/:id', async (args) => {
<button class="icon-btn" id="sb-share" title="Teilen" disabled>📤</button> <button class="icon-btn" id="sb-share" title="Teilen" disabled>📤</button>
`; `;
document.body.appendChild(bar); document.body.appendChild(bar);
bar.querySelector('#sb-cancel').onclick = endSelectMode; bar.querySelector('#sb-cancel').onclick = cancelViaBack;
bar.querySelector('#sb-all').onclick = () => { bar.querySelector('#sb-all').onclick = () => {
const thumbs = photoGrid.querySelectorAll('.thumb[data-relpath]'); const items = allSelectables();
const anyUn = Array.from(thumbs).some(t => !t.classList.contains('selected')); const anyUn = items.some(t => !t.classList.contains('selected'));
thumbs.forEach(t => t.classList.toggle('selected', anyUn)); items.forEach(t => t.classList.toggle('selected', anyUn));
updateSelectBar(); updateSelectBar();
}; };
bar.querySelector('#sb-share').onclick = () => shareSelected(); bar.querySelector('#sb-share').onclick = () => shareSelected();
} }
function updateSelectBar() { function updateSelectBar() {
const n = photoGrid.querySelectorAll('.thumb.selected').length; const n = allSelectables().filter(x => x.classList.contains('selected')).length;
const info = document.getElementById('sb-info'); const info = document.getElementById('sb-info');
const share = document.getElementById('sb-share'); const share = document.getElementById('sb-share');
if (info) info.textContent = n + ' ausgewählt'; if (info) info.textContent = n + ' ausgewählt';
if (share) share.disabled = n === 0; if (share) share.disabled = n === 0;
} }
async function shareSelected() { async function shareSelected() {
const sel = Array.from(photoGrid.querySelectorAll('.thumb.selected')); const sel = allSelectables().filter(x => x.classList.contains('selected'));
if (!sel.length) return; if (!sel.length) return;
showToast('Lade ' + sel.length + ' Foto' + (sel.length === 1 ? '' : 's') + '…'); showToast('Lade ' + sel.length + ' Datei' + (sel.length === 1 ? '' : 'en') + '…');
const items = []; const items = [];
for (const t of sel) { for (const t of sel) {
const rp = t.dataset.relpath; const rp = t.dataset.relpath;
try { try {
const f = await api.getFileBlobUrl(rp); const f = await api.getFileBlobUrl(rp);
if (f) items.push({ blob: f.blob, filename: rp.split('/').pop(), mime: f.mime }); if (f) items.push({ blob: f.blob, filename: t.dataset.filename || rp.split('/').pop(), mime: f.mime });
} catch (e) { console.warn('Share-Load', rp, e); } } catch (e) { console.warn('Share-Load', rp, e); }
} }
if (!items.length) { showToast('Keine Dateien geladen', 'error'); return; } if (!items.length) { showToast('Keine Dateien geladen', 'error'); return; }
const ok = await shareFiles(items, 'Fotos vom Auftrag'); const ok = await shareFiles(items, 'Dateien vom Auftrag');
if (ok) endSelectMode(); if (ok) cancelViaBack();
} }
// Weitere Dokumente: Tap = Öffnen (PDF inline, sonst Download) // Weitere Dokumente: Tap = Öffnen (oder Auswahl im Select-Modus)
document.querySelectorAll('.doc-list .doc-item').forEach(el => { document.querySelectorAll('.doc-list .doc-item').forEach(el => {
el.addEventListener('click', async () => { el.addEventListener('click', async (e) => {
if (docList && docList.classList.contains('selecting')) {
// Auch Klick auf Öffnen-Button soll nur selektieren
e.preventDefault();
e.stopPropagation();
el.classList.toggle('selected');
updateSelectBar();
return;
}
const rel = el.dataset.relpath; const rel = el.dataset.relpath;
const filename = el.dataset.filename; const filename = el.dataset.filename;
el.classList.add('loading'); el.classList.add('loading');