PWA: Dokumente (PDFs) in Mehrfachauswahl + Back-Button schließt Auswahl [deploy]
All checks were successful
Deploy baustelle-pwa / deploy (push) Successful in 4s
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:
parent
427fdeb0c0
commit
0a78d9e5d9
2 changed files with 73 additions and 14 deletions
24
app.css
24
app.css
|
|
@ -352,6 +352,30 @@ body {
|
|||
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 */
|
||||
.select-bar {
|
||||
position: fixed;
|
||||
|
|
|
|||
63
app.js
63
app.js
|
|
@ -45,6 +45,9 @@ function setBack(visible, hash) {
|
|||
const modalStack = [];
|
||||
let _skipNextPopstate = false;
|
||||
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) {
|
||||
modalStack.push({ el, cleanup: cleanup || null });
|
||||
|
|
@ -83,6 +86,12 @@ window.addEventListener('popstate', () => {
|
|||
try { top.cleanup && top.cleanup(); } catch {}
|
||||
return;
|
||||
}
|
||||
if (selectModeCleanup) {
|
||||
const fn = selectModeCleanup;
|
||||
selectModeCleanup = null;
|
||||
try { fn(); } catch {}
|
||||
return;
|
||||
}
|
||||
if (isTopLevelHash(location.hash)) {
|
||||
const now = Date.now();
|
||||
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="photo-section-head">
|
||||
<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 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('')}
|
||||
|
|
@ -558,6 +567,7 @@ router.on('/orders/:id', async (args) => {
|
|||
<h3>Weitere Dokumente (${otherDocs.length})</h3>
|
||||
<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)}">
|
||||
<div class="doc-check">✓</div>
|
||||
<span class="doc-icon">${docIconFor(p.filename, p.mime)}</span>
|
||||
<div class="doc-meta">
|
||||
<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)
|
||||
const photoGrid = document.getElementById('photo-grid');
|
||||
const docList = document.querySelector('.doc-list');
|
||||
photoGrid.querySelectorAll('.thumb').forEach(t => {
|
||||
t.addEventListener('click', () => {
|
||||
if (photoGrid.classList.contains('selecting')) {
|
||||
|
|
@ -586,17 +597,33 @@ router.on('/orders/:id', async (args) => {
|
|||
const selectBtn = document.getElementById('btn-select-mode');
|
||||
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() {
|
||||
if (photoGrid.classList.contains('selecting')) return;
|
||||
photoGrid.classList.add('selecting');
|
||||
if (docList) docList.classList.add('selecting');
|
||||
renderSelectBar();
|
||||
updateSelectBar();
|
||||
// Android-Back soll nur die Auswahl aufheben
|
||||
history.pushState({ _selecting: true }, '', location.hash);
|
||||
selectModeCleanup = endSelectMode;
|
||||
}
|
||||
function endSelectMode() {
|
||||
selectModeCleanup = null;
|
||||
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');
|
||||
if (bar) bar.remove();
|
||||
}
|
||||
function cancelViaBack() {
|
||||
if (history.state && history.state._selecting) history.back();
|
||||
else endSelectMode();
|
||||
}
|
||||
function renderSelectBar() {
|
||||
if (document.getElementById('select-bar')) return;
|
||||
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>
|
||||
`;
|
||||
document.body.appendChild(bar);
|
||||
bar.querySelector('#sb-cancel').onclick = endSelectMode;
|
||||
bar.querySelector('#sb-cancel').onclick = cancelViaBack;
|
||||
bar.querySelector('#sb-all').onclick = () => {
|
||||
const thumbs = photoGrid.querySelectorAll('.thumb[data-relpath]');
|
||||
const anyUn = Array.from(thumbs).some(t => !t.classList.contains('selected'));
|
||||
thumbs.forEach(t => t.classList.toggle('selected', anyUn));
|
||||
const items = allSelectables();
|
||||
const anyUn = items.some(t => !t.classList.contains('selected'));
|
||||
items.forEach(t => t.classList.toggle('selected', anyUn));
|
||||
updateSelectBar();
|
||||
};
|
||||
bar.querySelector('#sb-share').onclick = () => shareSelected();
|
||||
}
|
||||
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 share = document.getElementById('sb-share');
|
||||
if (info) info.textContent = n + ' ausgewählt';
|
||||
if (share) share.disabled = n === 0;
|
||||
}
|
||||
async function shareSelected() {
|
||||
const sel = Array.from(photoGrid.querySelectorAll('.thumb.selected'));
|
||||
const sel = allSelectables().filter(x => x.classList.contains('selected'));
|
||||
if (!sel.length) return;
|
||||
showToast('Lade ' + sel.length + ' Foto' + (sel.length === 1 ? '' : 's') + '…');
|
||||
showToast('Lade ' + sel.length + ' Datei' + (sel.length === 1 ? '' : 'en') + '…');
|
||||
const items = [];
|
||||
for (const t of sel) {
|
||||
const rp = t.dataset.relpath;
|
||||
try {
|
||||
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); }
|
||||
}
|
||||
if (!items.length) { showToast('Keine Dateien geladen', 'error'); return; }
|
||||
const ok = await shareFiles(items, 'Fotos vom Auftrag');
|
||||
if (ok) endSelectMode();
|
||||
const ok = await shareFiles(items, 'Dateien vom Auftrag');
|
||||
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 => {
|
||||
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 filename = el.dataset.filename;
|
||||
el.classList.add('loading');
|
||||
|
|
|
|||
Loading…
Reference in a new issue