// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // // Progressive enhancement only. Every interactive element works without this // file; it adds the mobile drawer, the theme picker, clipboard buttons, and a // couple of keyboard shortcuts. Kept small and dependency-free (< 200 lines). (function () { "use strict"; // ---- Mobile nav drawer ---- function toggleDrawer(open) { var body = document.body; var isOpen = open === undefined ? !body.classList.contains("drawer-open") : open; body.classList.toggle("drawer-open", isOpen); var toggle = document.querySelector("[data-drawer-toggle]"); if (toggle) toggle.setAttribute("aria-expanded", isOpen ? "true" : "false"); } document.addEventListener("click", function (e) { var toggle = e.target.closest("[data-drawer-toggle]"); if (toggle) { e.preventDefault(); toggleDrawer(); return; } if (e.target.closest("[data-drawer-close]") || e.target.classList.contains("drawer-backdrop")) { toggleDrawer(false); } }); // ---- Theme picker ---- // Apply the chosen theme instantly for feedback, then submit the picker's form // so the server persists the cookie and serves the matching stylesheet. Without // JS the form's submit button does the same thing. document.addEventListener("change", function (e) { var picker = e.target.closest("[data-theme-picker]"); if (!picker) return; var opt = picker.options[picker.selectedIndex]; var root = document.documentElement; root.setAttribute("data-theme", picker.value); if (opt && opt.getAttribute("data-scheme")) { root.style.colorScheme = opt.getAttribute("data-scheme"); } if (picker.form) picker.form.submit(); }); // ---- Dropdowns (details-based popovers) ---- // Native
stays open until its summary is clicked again; close any // open menu when a click lands outside it. Without JS the menus still work, // they just close on a second summary click. document.addEventListener("click", function (e) { document.querySelectorAll("details.clone-menu[open], details.branch-menu[open], details[data-menu][open]").forEach(function (d) { if (!d.contains(e.target)) d.removeAttribute("open"); }); }); // ---- Auto-submitting controls ---- // Any [data-autosubmit] control (select or checkbox) submits its form on // change; without JS a noscript button does the same. document.querySelectorAll("[data-autosubmit]").forEach(function (el) { el.addEventListener("change", function () { if (el.form) el.form.submit(); }); }); // ---- Label dropdown: show checked labels as chips inside the toggle ---- document.querySelectorAll("[data-label-dropdown]").forEach(function (dd) { var summaryLabel = dd.querySelector("[data-label-summary]"); if (!summaryLabel) return; function sync() { var checked = dd.querySelectorAll('input[type="checkbox"]:checked'); summaryLabel.textContent = ""; if (checked.length === 0) { summaryLabel.textContent = "Select labels"; return; } checked.forEach(function (cb) { var name = cb.getAttribute("data-label-name") || cb.value; var chip = document.createElement("span"); chip.style.setProperty("--label-color", cb.getAttribute("data-label-color") || "#888"); var sep = name.indexOf(": "); if (sep === -1) { chip.className = "label-chip"; chip.textContent = name; } else { // Scoped label: static markup, text set via textContent (escaped). chip.className = "label-chip label-chip-scoped"; chip.innerHTML = ''; chip.firstChild.textContent = name.slice(0, sep).trim(); chip.lastChild.textContent = name.slice(sep + 2).trim(); } summaryLabel.appendChild(chip); }); } dd.addEventListener("change", sync); sync(); }); // ---- Progressive link fields ---- // Show the filled links plus one empty slot; reveal more (up to the max) via // the Add link button. Without JS every slot is visible. document.querySelectorAll("[data-link-fields]").forEach(function (wrap) { var slots = Array.prototype.slice.call(wrap.querySelectorAll(".link-slot")); var addBtn = wrap.querySelector("[data-link-add]"); // Initial layout: show every filled slot plus one empty one, hide the rest. var firstEmptyShown = false; slots.forEach(function (s) { if (s.value.trim()) { s.hidden = false; } else if (!firstEmptyShown) { s.hidden = false; firstEmptyShown = true; } else { s.hidden = true; } }); // Keep the Add button beside the last visible slot; hide it once all show. function mark() { var lastVisible = null; slots.forEach(function (s) { s.classList.remove("last-visible"); if (!s.hidden) lastVisible = s; }); if (lastVisible) lastVisible.classList.add("last-visible"); if (addBtn) addBtn.hidden = !slots.some(function (s) { return s.hidden; }); } if (addBtn) { addBtn.addEventListener("click", function () { var next = slots.filter(function (s) { return s.hidden; })[0]; if (next) { next.hidden = false; next.focus(); } mark(); }); } mark(); }); // ---- Clipboard buttons ---- document.addEventListener("click", function (e) { var btn = e.target.closest("[data-clipboard]"); if (!btn) return; e.preventDefault(); var text = btn.getAttribute("data-clipboard"); if (navigator.clipboard) { navigator.clipboard.writeText(text).then(function () { var old = btn.getAttribute("aria-label") || btn.textContent; btn.setAttribute("aria-label", "Copied"); btn.classList.add("copied"); setTimeout(function () { btn.setAttribute("aria-label", old); btn.classList.remove("copied"); }, 1200); }); } }); // ---- Keyboard shortcuts ---- document.addEventListener("keydown", function (e) { // Ignore when typing in a field. var tag = (e.target.tagName || "").toLowerCase(); if (tag === "input" || tag === "textarea" || tag === "select" || e.target.isContentEditable) { if (e.key === "Escape") e.target.blur(); return; } if (e.key === "/") { var search = document.querySelector('input[type="search"], [data-search-input]'); if (search) { e.preventDefault(); search.focus(); } } else if (e.key === "Escape") { toggleDrawer(false); document.querySelectorAll("details.clone-menu[open], details.branch-menu[open], details[data-menu][open]").forEach(function (d) { d.removeAttribute("open"); }); } }); // ---- Release asset dropzone: pick or drop a file to upload it immediately ---- document.querySelectorAll(".asset-upload").forEach(function (form) { var input = form.querySelector("input[type=file]"); var zone = form.querySelector(".dropzone"); if (!input || !zone) return; var submit = form.querySelector("button[type=submit]"); if (submit) submit.hidden = true; // JS uploads on selection; button is no-JS only input.addEventListener("change", function () { if (input.files.length) form.submit(); }); ["dragover", "dragenter", "dragleave", "drop"].forEach(function (ev) { zone.addEventListener(ev, function (e) { e.preventDefault(); zone.classList.toggle("drag", ev === "dragover" || ev === "dragenter"); if (ev === "drop" && e.dataTransfer.files.length) { input.files = e.dataTransfer.files; form.submit(); } }); }); }); })();