diff --git a/app/static/js/main.js b/app/static/js/main.js index dcd17d2..6c51c6b 100644 --- a/app/static/js/main.js +++ b/app/static/js/main.js @@ -481,15 +481,79 @@ const DATA_ACTIONS = { event.preventDefault(); history.back(); }, + // Removes the nearest ancestor matching data-remove, or the parent. + 'remove-element': function (element) { + const selector = element.getAttribute('data-remove'); + const target = selector ? element.closest(selector) : element.parentElement; + if (target) { + target.remove(); + } + }, }; -document.addEventListener('click', function (event) { - const trigger = event.target.closest('[data-action]'); +/** + * Register behaviours defined by a single page. + * + * Page-local functions live in that page's script block, so they cannot be + * listed in DATA_ACTIONS above. Each page declares its own: + * + * registerActions({ 'clear-availability': clearAllAvailability }); + * + * @param {Object} map - action name to handler(element, event). + */ +function registerActions(map) { + Object.assign(DATA_ACTIONS, map); +} + +function dispatchAction(attribute, event) { + const trigger = event.target.closest('[' + attribute + ']'); if (!trigger) { return; } - const handler = DATA_ACTIONS[trigger.getAttribute('data-action')]; + const handler = DATA_ACTIONS[trigger.getAttribute(attribute)]; if (handler) { handler(trigger, event); } +} + +document.addEventListener('click', function (event) { + dispatchAction('data-action', event); +}); + +// Separate attribute rather than one shared with click: a + * + * {value} is replaced by the chosen option, URL-encoded. An empty + * selection navigates nowhere. + */ +document.addEventListener('change', function (event) { + const select = event.target.closest('[data-navigate]'); + if (!select || !select.value) { + return; + } + window.location.href = select.getAttribute('data-navigate') + .replace('{value}', encodeURIComponent(select.value)); }); diff --git a/app/templates/pages/coach_availability.html b/app/templates/pages/coach_availability.html index 7cd24f8..b70939b 100644 --- a/app/templates/pages/coach_availability.html +++ b/app/templates/pages/coach_availability.html @@ -15,7 +15,7 @@
-
@@ -134,7 +134,7 @@ function renderGrid() { TIME_SLOTS.forEach(slot => { const isSelected = selectedSlots[dayIndex] && selectedSlots[dayIndex].includes(slot.time); const cssClass = isSelected ? 'time-slot selected' : 'time-slot'; - html += '
' + slot.display + '
'; + html += '
' + slot.display + '
'; }); html += ''; @@ -143,7 +143,9 @@ function renderGrid() { grid.innerHTML = html; } -function toggleSlot(dayOfWeek, timeStr, element) { +function toggleSlot(element) { + const dayOfWeek = Number(element.getAttribute('data-day')); + const timeStr = element.getAttribute('data-time'); if (!selectedSlots[dayOfWeek]) { selectedSlots[dayOfWeek] = []; } @@ -203,7 +205,7 @@ function flash(message, type) { const flashContainer = document.querySelector('.flash-messages'); const alert = document.createElement('div'); alert.className = 'alert alert-' + type + ' alert-dismissible'; - alert.innerHTML = '' + message + ''; + alert.innerHTML = '' + message + ''; flashContainer.appendChild(alert); } @@ -215,5 +217,13 @@ document.addEventListener('click', function(e) { saveTimeout = setTimeout(saveAvailability, 1000); } }); + +// Behaviours are declared in the markup with data-action / data-change and +// dispatched by the delegated listener in main.js. This replaces inline +// onclick attributes, which no CSP nonce is able to authorise. +registerActions({ + 'clear-availability': clearAllAvailability, + 'toggle-slot': toggleSlot, +}); {% endblock %} \ No newline at end of file diff --git a/app/templates/pages/contracts.html b/app/templates/pages/contracts.html index 088607e..befddcf 100644 --- a/app/templates/pages/contracts.html +++ b/app/templates/pages/contracts.html @@ -55,7 +55,7 @@ {% endif %} {% if current_user.role == 'player' and contract.status == 'pending' %} - {% endif %} @@ -83,11 +83,11 @@ {% if current_user.role == 'player' %}