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;
|
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
63
app.js
|
|
@ -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');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue