/* * trip-publish.js (U6) — owner publish/unpublish toggle for the /trips listing. * * Loaded only for the owner (trips.html.twig gates the asset). Each listing card * carries a switch (partials/trip-publish-toggle.html.twig) overlaid on the cover * as a sibling of the navigating , so toggling never follows the card link. * * Flow: click/Enter/Space on the switch → (if unpublishing the ACTIVE trip) * window.confirm → lock the switch (aria-busy) → POST /api/v1/trip//publish * {published} (session cookie) → on success flip the switch + Draft badge in place * (no reload); on failure re-enable and surface a VISIBLE page-level toast. * * Markup contract (data-* on button.trip-publish-toggle): * data-trip-slug, data-published ("true"|"false"), * data-active ("true" when this is site.active_trip); aria-checked mirrors * data-published. The sibling .trip-draft-badge is shown iff not published. */ import { apiSend, apiErrorMsg } from './api-utils.js'; var TOAST_TIMEOUT_MS = 5000; var PUBLISH_TIMEOUT_MS = 10000; // abort a hung toggle so the switch never sticks (R13) var toastTimer = null; // One visible, page-level polite toast (R15). Modelled on the feed-actions.js // live region but intentionally NOT sr-only: the trip card has no inline message // slot, so a sighted owner must actually see the failure. Replaces (never queues) // the message, auto-dismisses, and carries a manual close control. function toastEl() { var el = document.getElementById('trip-publish-live'); if (!el) { el = document.createElement('div'); el.id = 'trip-publish-live'; el.className = 'trip-publish-toast'; el.setAttribute('role', 'status'); el.setAttribute('aria-live', 'polite'); el.hidden = true; var msg = document.createElement('span'); msg.className = 'trip-publish-toast__msg'; var close = document.createElement('button'); close.type = 'button'; close.className = 'trip-publish-toast__close'; close.setAttribute('aria-label', 'Dismiss'); close.textContent = '×'; // × close.addEventListener('click', hideToast); el.appendChild(msg); el.appendChild(close); document.body.appendChild(el); } return el; } function showToast(message) { var el = toastEl(); el.querySelector('.trip-publish-toast__msg').textContent = message; el.hidden = false; if (toastTimer) clearTimeout(toastTimer); toastTimer = setTimeout(hideToast, TOAST_TIMEOUT_MS); } function hideToast() { var el = document.getElementById('trip-publish-live'); if (el) el.hidden = true; if (toastTimer) { clearTimeout(toastTimer); toastTimer = null; } } function setPublishedUI(btn, published) { btn.setAttribute('aria-checked', published ? 'true' : 'false'); btn.setAttribute('data-published', published ? 'true' : 'false'); var overlay = btn.closest('.trip-publish-overlay'); var badge = overlay ? overlay.querySelector('.trip-draft-badge') : null; if (badge) badge.hidden = published; } function setPending(btn, pending) { if (pending) { btn.setAttribute('aria-busy', 'true'); btn.disabled = true; } else { btn.removeAttribute('aria-busy'); btn.disabled = false; } } function onToggle(btn) { if (btn.getAttribute('aria-busy') === 'true') return; // already in flight (R13) var current = btn.getAttribute('data-published') === 'true'; var next = !current; // R12: unpublishing the ACTIVE trip removes it from the home page — confirm // first; cancelling leaves it published (no request, no UI change). if (!next && btn.getAttribute('data-active') === 'true') { if (!window.confirm('This is your active trip — unpublishing it also removes it from the home page. Unpublish anyway?')) { return; } } var slug = btn.getAttribute('data-trip-slug'); setPending(btn, true); apiSend('/api/v1/trip/' + encodeURIComponent(slug) + '/publish', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify({ published: next }) }, null, PUBLISH_TIMEOUT_MS).then(function () { // Success: flip the switch + Draft badge in place, no reload (R14). setPublishedUI(btn, next); setPending(btn, false); }).catch(function (err) { // Failure: the UI never flipped, so revert is just re-enable (R15). setPending(btn, false); showToast(apiErrorMsg(err, "Couldn't update — try again.")); }); } function initTripPublish() { document.addEventListener('click', function (e) { var btn = e.target.closest ? e.target.closest('.trip-publish-toggle') : null; if (!btn) return; e.preventDefault(); onToggle(btn); }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initTripPublish); } else { initTripPublish(); }