/* * feed-actions.js (U6) — owner delete wiring for journal feed cards. * * Loaded only on the trip / home active-trip feed for the owner. The Edit link is * a plain navigation (no JS). Delete is a two-step inline confirm (no browser * dialog): Delete → Cancel / Confirm delete → DELETE /api/v1/entry/ * (session cookie), then the card is removed and focus moves to the next card. * * Markup (rendered by partials/entry-journal.html.twig): * article.journal-post[data-entry-route] * .journal-post-actions[data-entry-route] * button[data-delete-start] (Delete) * .entry-delete-confirm[hidden] * button[data-delete-cancel] (Cancel) * button[data-delete-confirm] (Confirm delete) * .entry-delete-msg[aria-live] (inline error slot) */ // One page-level polite live region for cross-card announcements (D4) — the // per-card .entry-delete-msg vanishes with the card it belongs to. function liveRegion() { var el = document.getElementById('feed-actions-live'); if (!el) { el = document.createElement('div'); el.id = 'feed-actions-live'; el.setAttribute('aria-live', 'polite'); el.setAttribute('role', 'status'); el.className = 'sr-only'; document.body.appendChild(el); } return el; } function announce(msg) { liveRegion().textContent = msg; } function slugFromRoute(route) { var parts = String(route || '').replace(/\/+$/, '').split('/'); return parts[parts.length - 1] || ''; } function showConfirm(actions, show) { var del = actions.querySelector('[data-delete-start]'); var confirm = actions.querySelector('.entry-delete-confirm'); if (del) del.hidden = show; if (confirm) confirm.hidden = !show; if (!show) { var msg = actions.querySelector('.entry-delete-msg'); if (msg) msg.textContent = ''; } } function performDelete(actions) { var route = actions.getAttribute('data-entry-route'); var slug = slugFromRoute(route); var cancelBtn = actions.querySelector('[data-delete-cancel]'); var confirmBtn = actions.querySelector('[data-delete-confirm]'); var msg = actions.querySelector('.entry-delete-msg'); var card = actions.closest('.journal-post'); // D2: lock both buttons immediately so a mobile double-tap can't fire two // DELETEs (the second would 404 on an already-removed folder). if (confirmBtn) { confirmBtn.disabled = true; confirmBtn.textContent = 'Deleting…'; } if (cancelBtn) cancelBtn.disabled = true; if (msg) msg.textContent = ''; announce('Deleting entry…'); fetch('/api/v1/entry/' + encodeURIComponent(slug), { method: 'DELETE', credentials: 'include', headers: { Accept: 'application/json' } }).then(function (r) { if (!(r.status === 204 || r.ok)) throw new Error('HTTP ' + r.status); // D4: move focus to the next journal card (or the feed heading) BEFORE // removing this one, then announce. var next = card ? card.nextElementSibling : null; while (next && !next.classList.contains('journal-post')) next = next.nextElementSibling; var focusTarget = next || (card && card.previousElementSibling && card.previousElementSibling.classList && card.previousElementSibling.classList.contains('journal-post') ? card.previousElementSibling : null) || document.querySelector('.home-trip-name, .home-feed-col h1, .feed'); if (card) card.remove(); if (focusTarget) { if (!focusTarget.hasAttribute('tabindex')) focusTarget.setAttribute('tabindex', '-1'); try { focusTarget.focus({ preventScroll: false }); } catch (e) { /* older browsers */ } } announce('Entry deleted.'); }).catch(function () { // D7: restore the control + inline error, constrained to the card. if (confirmBtn) { confirmBtn.disabled = false; confirmBtn.textContent = 'Confirm delete'; } if (cancelBtn) cancelBtn.disabled = false; if (msg) msg.textContent = 'Could not delete — please try again.'; announce('Delete failed.'); }); } function initFeedActions() { document.addEventListener('click', function (e) { var start = e.target.closest ? e.target.closest('[data-delete-start]') : null; var cancel = e.target.closest ? e.target.closest('[data-delete-cancel]') : null; var confirm = e.target.closest ? e.target.closest('[data-delete-confirm]') : null; if (!start && !cancel && !confirm) return; var actions = (start || cancel || confirm).closest('.journal-post-actions'); if (!actions) return; e.preventDefault(); if (start) { showConfirm(actions, true); return; } if (cancel) { showConfirm(actions, false); return; } if (confirm) { var btn = actions.querySelector('[data-delete-confirm]'); if (btn && btn.disabled) return; // already in flight (D2) performDelete(actions); } }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initFeedActions); } else { initFeedActions(); }