Match videos: full re-hydration on Up Next SPA navigation
Fixes a raft of things breaking when jumping between match videos via the
Up Next / playlist sidebar without a page reload:
- Highlights toggle stopped working (button held a click handler pointing at
the OLD, detached .events-sidebar node).
- Points / Coach review tabs went unclickable (new .tab-button elements had
no bound handlers).
- Event-item clicks (jump-to-timestamp on point / review cards) stopped
seeking / playing.
- window.matchRounds / window.matchReviews and const videoId were frozen at
first page load, so edit/delete/save handlers routed to the wrong video.
- Blade point card leaked "{{ $fmtTime(...) }}" as literal text because of
a misused @{{ }} escape directive.
Changes
- videoId + isOwner now live on window so they can be updated at runtime;
`var videoId` at module scope keeps existing handlers working.
- Extracted tab-button + attachEventListeners wiring into
window.initMatchTabsAndEvents (idempotent — _tabBound flag).
- Added window.reloadMatchVideoState(url): parses the new slug from the URL,
updates window.videoId, calls loadMatchData (refreshes cache + re-renders
sidebar), then re-runs the tabs/events/toggle/sidebar-height inits.
- recSwapContent and plSwapContent call reloadMatchVideoState whenever the
target page has an .events-sidebar (falls back to just the toggle re-init
for non-match targets).
- Fixed the Blade point-card time: {{ '@' . $fmtTime(...) }} instead of
@{{ ... }} so the timestamp actually renders instead of showing braces.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
716693a74f
commit
cba8e01d56
@ -2419,14 +2419,24 @@
|
||||
its click listener and the Highlights pane can't be opened. --}}
|
||||
<script>
|
||||
// Match Highlights Toggle with localStorage persistence
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Extracted into a globally-callable initializer so SPA navigation
|
||||
// (recSwapContent / plSwapContent) can re-bind the click handler
|
||||
// against the FRESH .events-sidebar node after the sidebar is
|
||||
// swapped in from the target page.
|
||||
window.initMatchHighlightsToggle = function () {
|
||||
const toggleBtn = document.getElementById('matchHighlightsToggle');
|
||||
const sidebar = document.querySelector('.events-sidebar');
|
||||
// NOTE: emit videoId with the json Blade directive (raw), never with
|
||||
// escaped echo braces — those HTML-escape the quotes and crash the script.
|
||||
const videoId = @json(isset($video) ? $video->getRouteKey() : '');
|
||||
// Prefer the freshest videoId in case SPA nav updated it
|
||||
const videoId = window.videoId || @json(isset($video) ? $video->getRouteKey() : '');
|
||||
if (toggleBtn && toggleBtn._hlBound && sidebar && toggleBtn._hlSidebar === sidebar) {
|
||||
return; // Already bound to this exact sidebar node — nothing to do
|
||||
}
|
||||
|
||||
if (toggleBtn && sidebar) {
|
||||
// Wipe any prior click listener bound to a stale sidebar
|
||||
if (toggleBtn._hlHandler) toggleBtn.removeEventListener('click', toggleBtn._hlHandler);
|
||||
toggleBtn._hlBound = true;
|
||||
toggleBtn._hlSidebar = sidebar;
|
||||
const backdrop = document.getElementById('hlSheetBackdrop');
|
||||
const grab = sidebar.querySelector('.hl-sheet-grab');
|
||||
const mainScroll = document.getElementById('main');
|
||||
@ -2471,7 +2481,8 @@
|
||||
setOpen(true);
|
||||
}
|
||||
|
||||
toggleBtn.addEventListener('click', () => setOpen(!sidebar.classList.contains('show')));
|
||||
toggleBtn._hlHandler = () => setOpen(!sidebar.classList.contains('show'));
|
||||
toggleBtn.addEventListener('click', toggleBtn._hlHandler);
|
||||
if (backdrop) backdrop.addEventListener('click', () => setOpen(false));
|
||||
if (grab) grab.addEventListener('click', () => setOpen(false));
|
||||
|
||||
@ -2508,7 +2519,13 @@
|
||||
document.addEventListener('webkitfullscreenchange', onFsChange);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
// First run on initial page load
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', window.initMatchHighlightsToggle);
|
||||
} else {
|
||||
window.initMatchHighlightsToggle();
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@ -2573,7 +2590,7 @@
|
||||
@endphp
|
||||
<div class="event-item" data-time-start="{{ (int) $point->timestamp_seconds }}"
|
||||
data-round="{{ $round->round_number }}" data-id="pt{{ $point->id }}">
|
||||
<div class="event-time">@{{ $fmtTime($point->timestamp_seconds) }}</div>
|
||||
<div class="event-time">{{ '@' . $fmtTime($point->timestamp_seconds) }}</div>
|
||||
<div>
|
||||
<div class="event-label">
|
||||
{{ $label }}
|
||||
@ -2810,6 +2827,14 @@
|
||||
document.body.removeChild(ns);
|
||||
}
|
||||
}
|
||||
// Full re-hydration for match videos when navigating
|
||||
// between them from a playlist
|
||||
if (document.querySelector('.events-sidebar') &&
|
||||
typeof window.reloadMatchVideoState === 'function') {
|
||||
window.reloadMatchVideoState(url);
|
||||
} else if (typeof window.initMatchHighlightsToggle === 'function') {
|
||||
window.initMatchHighlightsToggle();
|
||||
}
|
||||
} catch(e){ console.warn('plSwapContent',e); }
|
||||
}
|
||||
|
||||
@ -3078,6 +3103,14 @@
|
||||
var ns=document.createElement('script'); ns.textContent=s.textContent; document.body.appendChild(ns); document.body.removeChild(ns);
|
||||
});
|
||||
}
|
||||
// Full re-hydration for match videos: updates videoId,
|
||||
// reloads match data, rebinds tabs / clicks / toggle.
|
||||
if (document.querySelector('.events-sidebar') &&
|
||||
typeof window.reloadMatchVideoState === 'function') {
|
||||
window.reloadMatchVideoState(url);
|
||||
} else if (typeof window.initMatchHighlightsToggle === 'function') {
|
||||
window.initMatchHighlightsToggle();
|
||||
}
|
||||
} catch(e){ console.warn('recSwapContent', e); }
|
||||
}
|
||||
|
||||
@ -3657,11 +3690,14 @@
|
||||
});
|
||||
}
|
||||
|
||||
// NOTE: emit these with the json Blade directive (raw output). Escaped echo
|
||||
// braces HTML-escape the quotes (videoId becomes an entity-encoded string) and
|
||||
// crash this whole block — which loads match data, switches tabs, drives CRUD.
|
||||
const videoId = @json(isset($video) ? $video->getRouteKey() : '');
|
||||
const isOwner = @json(Auth::check() && isset($video) && Auth::id() === $video->user_id);
|
||||
// These live on `window` so SPA navigation (recSwapContent) can update
|
||||
// them for the newly-loaded match without a page refresh. Downstream
|
||||
// code references `videoId` and `isOwner` as free identifiers, which
|
||||
// resolve to window.videoId / window.isOwner via the global scope.
|
||||
window.videoId = @json(isset($video) ? $video->getRouteKey() : '');
|
||||
window.isOwner = @json(Auth::check() && isset($video) && Auth::id() === $video->user_id);
|
||||
var videoId = window.videoId;
|
||||
var isOwner = window.isOwner;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadMatchData();
|
||||
@ -3903,19 +3939,28 @@
|
||||
initSidebarHeightSync();
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Extracted so SPA nav can rebind against the fresh .tab-button / .event-item
|
||||
// elements after the sidebar is swapped in.
|
||||
window.initMatchTabsAndEvents = function () {
|
||||
const tabButtons = document.querySelectorAll('.tab-button');
|
||||
const tabPanels = document.querySelectorAll('.tab-panel');
|
||||
const tabPanels = document.querySelectorAll('.tab-panel');
|
||||
tabButtons.forEach(button => {
|
||||
if (button._tabBound) return;
|
||||
button._tabBound = true;
|
||||
button.addEventListener('click', function() {
|
||||
const targetTab = this.getAttribute('data-tab');
|
||||
tabButtons.forEach(btn => btn.classList.remove('active'));
|
||||
tabPanels.forEach(panel => panel.classList.remove('active'));
|
||||
// Re-query on every click — the sidebar may have been re-rendered
|
||||
document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-panel').forEach(panel => panel.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
document.getElementById('tab-' + targetTab).classList.add('active');
|
||||
const target = document.getElementById('tab-' + targetTab);
|
||||
if (target) target.classList.add('active');
|
||||
});
|
||||
});
|
||||
attachEventListeners();
|
||||
};
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
window.initMatchTabsAndEvents();
|
||||
document.getElementById('addRoundForm')?.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
storeRound(
|
||||
@ -3925,14 +3970,31 @@
|
||||
document.getElementById('roundStartTime').value
|
||||
);
|
||||
});
|
||||
// pointForm removed — inline capture strip handles submit
|
||||
document.getElementById('editRoundForm')?.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
updateRound();
|
||||
});
|
||||
// reviewForm removed — inline review-capture strip handles submit
|
||||
});
|
||||
|
||||
// Called from recSwapContent / plSwapContent when SPA navigation lands
|
||||
// on a different match video. Updates window.videoId from the URL, then
|
||||
// reloads the match data and re-binds all UI handlers.
|
||||
window.reloadMatchVideoState = function (url) {
|
||||
try {
|
||||
const path = new URL(url, window.location.origin).pathname;
|
||||
const parts = path.split('/').filter(Boolean);
|
||||
const idx = parts.indexOf('videos');
|
||||
if (idx !== -1 && parts[idx + 1]) window.videoId = parts[idx + 1];
|
||||
} catch (_) {}
|
||||
// Keep the local `videoId` reference in sync for handlers that
|
||||
// captured the free identifier at module scope
|
||||
try { videoId = window.videoId; } catch (_) {}
|
||||
if (typeof loadMatchData === 'function') loadMatchData();
|
||||
if (typeof window.initMatchTabsAndEvents === 'function') window.initMatchTabsAndEvents();
|
||||
if (typeof window.initMatchHighlightsToggle === 'function') window.initMatchHighlightsToggle();
|
||||
if (typeof initSidebarHeightSync === 'function') initSidebarHeightSync();
|
||||
};
|
||||
|
||||
let reviewPlaybackStopHandler = null;
|
||||
let reviewPlaybackEndTime = null;
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user