| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119 |
- /*
- * Copyright 2026 Alessandro Chiapparini <sprint_planer_web@chiapparini.org>
- * SPDX-License-Identifier: Apache-2.0
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * See the LICENSE file in the project root for the full license text.
- */
- /**
- * Site-wide behaviour. Three concerns:
- * 1. Vanilla-JS click delegation for [data-href] rows (sprint list).
- * 2. Alpine CSP component: appMenu (hamburger).
- * 3. Alpine CSP component: themeToggle (dark-mode flip).
- *
- * htmx is loaded site-wide and sends X-CSRF-Token automatically per the
- * `htmx:configRequest` listener below.
- */
- (function () {
- 'use strict';
- // ----- data-href click delegation ------------------------------------
- function navigate(el, ev) {
- const href = String(el.getAttribute('data-href') || '');
- if (href === '') { return; }
- if (ev.metaKey || ev.ctrlKey || ev.button === 1) {
- window.open(href, '_blank');
- } else {
- window.location.href = href;
- }
- }
- document.addEventListener('click', function (ev) {
- const el = ev.target.closest('[data-href]');
- if (!el) { return; }
- // Ignore clicks that originated on an interactive descendant.
- if (ev.target.closest('a, button, input, select, textarea, label')) { return; }
- navigate(el, ev);
- });
- document.addEventListener('keydown', function (ev) {
- if (ev.key !== 'Enter') { return; }
- const el = ev.target.closest('[data-href]');
- if (!el) { return; }
- if (ev.target.closest('a, button, input, select, textarea, label')) { return; }
- ev.preventDefault();
- window.location.href = String(el.getAttribute('data-href') || '');
- });
- // Make every data-href row keyboard-reachable + screenreader-friendly.
- document.querySelectorAll('[data-href]').forEach(function (el) {
- el.setAttribute('role', 'link');
- el.setAttribute('tabindex', '0');
- });
- // ----- htmx CSRF wiring ---------------------------------------------
- // The CSRF token lives on every layout's form (_csrf hidden input) and on
- // the [data-sprint-root] element (data-csrf). Pull from a hidden input if
- // present so non-sprint pages also get the header attached.
- function readCsrfToken() {
- const inp = document.querySelector('input[name="_csrf"]');
- if (inp && inp.value) { return inp.value; }
- const root = document.querySelector('[data-sprint-root]');
- if (root && root.getAttribute('data-csrf')) {
- return root.getAttribute('data-csrf');
- }
- return '';
- }
- document.addEventListener('htmx:configRequest', function (ev) {
- const tok = readCsrfToken();
- if (tok && ev.detail && ev.detail.headers) {
- ev.detail.headers['X-CSRF-Token'] = tok;
- }
- });
- // ----- Alpine component registrations -------------------------------
- document.addEventListener('alpine:init', function () {
- // Hamburger menu — toggle, close on outside / Escape / item click.
- window.Alpine.data('appMenu', function () {
- return {
- open: false,
- toggle() { this.open = !this.open; },
- close() { this.open = false; },
- closeAndFocus() {
- if (!this.open) { return; }
- this.open = false;
- if (this.$refs.trigger) { this.$refs.trigger.focus(); }
- },
- closeOnItem(ev) {
- if (ev.target.closest('[role="menuitem"]')) { this.open = false; }
- },
- };
- });
- // Theme toggle — flips <html class="dark">, persists in localStorage,
- // mirrors a "Dark" / "Light" label into the menu row.
- window.Alpine.data('themeToggle', function () {
- return {
- label: '',
- init() {
- this.stamp();
- // If theme-init.js loaded after Alpine for some reason,
- // reflect the current class state on next tick.
- this.$nextTick(() => this.stamp());
- },
- stamp() {
- this.label = document.documentElement.classList.contains('dark') ? 'Dark' : 'Light';
- },
- flip() {
- const nowDark = document.documentElement.classList.toggle('dark');
- try { localStorage.setItem('sp:theme', nowDark ? 'dark' : 'light'); }
- catch (e) { /* private mode quota — ignore */ }
- this.stamp();
- },
- };
- });
- });
- })();
|