app.js
4,574 bytes
| 1 | const copyButton = document.querySelector("#copy-link"); |
|---|---|
| 2 | |
| 3 | async function copyText(button, value) { |
| 4 | const originalLabel = button.textContent; |
| 5 | try { |
| 6 | await navigator.clipboard.writeText(value); |
| 7 | button.textContent = "Copied"; |
| 8 | } catch { |
| 9 | button.textContent = "Copy failed"; |
| 10 | } |
| 11 | window.setTimeout(() => { |
| 12 | button.textContent = originalLabel; |
| 13 | }, 1800); |
| 14 | } |
| 15 | |
| 16 | copyButton?.addEventListener("click", () => { |
| 17 | const shareUrl = document.querySelector("#share-url"); |
| 18 | copyText(copyButton, shareUrl.value); |
| 19 | }); |
| 20 | |
| 21 | document.querySelectorAll("[data-copy-text]").forEach((button) => { |
| 22 | button.addEventListener("click", (event) => { |
| 23 | // Some copy buttons sit inside a <summary>, where a plain click would also |
| 24 | // collapse the file the reader just asked about. |
| 25 | event.preventDefault(); |
| 26 | copyText(button, button.dataset.copyText); |
| 27 | }); |
| 28 | }); |
| 29 | |
| 30 | const WRAP_STORAGE_KEY = "profileshare-diff-wrap"; |
| 31 | |
| 32 | function readStoredWrap() { |
| 33 | try { |
| 34 | return window.localStorage.getItem(WRAP_STORAGE_KEY) === "on"; |
| 35 | } catch { |
| 36 | return false; |
| 37 | } |
| 38 | } |
| 39 | |
| 40 | document.querySelectorAll("[data-diff-wrap]").forEach((button) => { |
| 41 | function apply(wrapped) { |
| 42 | document.body.classList.toggle("diff-wrapped", wrapped); |
| 43 | button.setAttribute("aria-pressed", String(wrapped)); |
| 44 | button.textContent = wrapped ? "No wrap" : "Wrap lines"; |
| 45 | } |
| 46 | apply(readStoredWrap()); |
| 47 | button.addEventListener("click", () => { |
| 48 | const wrapped = !document.body.classList.contains("diff-wrapped"); |
| 49 | apply(wrapped); |
| 50 | try { |
| 51 | window.localStorage.setItem(WRAP_STORAGE_KEY, wrapped ? "on" : "off"); |
| 52 | } catch { |
| 53 | // A blocked storage quota is not a reason to lose the toggle itself. |
| 54 | } |
| 55 | }); |
| 56 | }); |
| 57 | |
| 58 | document.querySelectorAll("[data-diff-toggle-all]").forEach((button) => { |
| 59 | const files = [...document.querySelectorAll("details.diff-file")]; |
| 60 | button.addEventListener("click", () => { |
| 61 | const collapse = files.some((file) => file.open); |
| 62 | files.forEach((file) => { file.open = !collapse; }); |
| 63 | button.textContent = collapse ? "Expand all" : "Collapse all"; |
| 64 | }); |
| 65 | }); |
| 66 | |
| 67 | document.addEventListener("keydown", (event) => { |
| 68 | if (event.key !== "/" || event.ctrlKey || event.metaKey || event.altKey) return; |
| 69 | if (event.target.closest("input, textarea, select, [contenteditable]")) return; |
| 70 | const searchInput = document.querySelector("[data-search-input]"); |
| 71 | if (!searchInput) return; |
| 72 | event.preventDefault(); |
| 73 | searchInput.focus(); |
| 74 | }); |
| 75 | |
| 76 | document.querySelectorAll("[data-ref-filter]").forEach((input) => { |
| 77 | const menu = input.closest(".ref-menu"); |
| 78 | const options = [...menu.querySelectorAll("[data-ref-option]")]; |
| 79 | const emptyState = menu.querySelector("[data-ref-empty]"); |
| 80 | input.addEventListener("input", () => { |
| 81 | const query = input.value.trim().toLocaleLowerCase("en"); |
| 82 | let visible = 0; |
| 83 | options.forEach((option) => { |
| 84 | option.hidden = !option.dataset.refName.includes(query); |
| 85 | if (!option.hidden) visible += 1; |
| 86 | }); |
| 87 | emptyState.hidden = visible > 0; |
| 88 | }); |
| 89 | input.addEventListener("keydown", (event) => { |
| 90 | if (event.key !== "Escape") return; |
| 91 | const details = input.closest("details"); |
| 92 | details.open = false; |
| 93 | details.querySelector("summary").focus(); |
| 94 | }); |
| 95 | }); |
| 96 | |
| 97 | document.addEventListener("click", (event) => { |
| 98 | document.querySelectorAll(".ref-switcher[open]").forEach((details) => { |
| 99 | if (!details.contains(event.target)) details.open = false; |
| 100 | }); |
| 101 | }); |
| 102 | |
| 103 | const shareForm = document.querySelector(".share-form"); |
| 104 | const repositoryInputs = [...document.querySelectorAll('input[name="repositories"]')]; |
| 105 | const selectionCount = document.querySelector("#selection-count"); |
| 106 | const createButton = shareForm?.querySelector('button[type="submit"]'); |
| 107 | const creationStatus = document.querySelector("#creation-status"); |
| 108 | |
| 109 | function updateSelection() { |
| 110 | const count = repositoryInputs.filter((input) => input.checked).length; |
| 111 | if (selectionCount) { |
| 112 | selectionCount.textContent = `${count} ${count === 1 ? "repository" : "repositories"} selected`; |
| 113 | } |
| 114 | if (createButton) createButton.disabled = count === 0; |
| 115 | } |
| 116 | |
| 117 | repositoryInputs.forEach((input) => input.addEventListener("change", updateSelection)); |
| 118 | updateSelection(); |
| 119 | |
| 120 | shareForm?.addEventListener("submit", () => { |
| 121 | createButton.disabled = true; |
| 122 | createButton.textContent = "Creating snapshot..."; |
| 123 | creationStatus.textContent = "Creating the snapshot. Large repositories may take a few minutes."; |
| 124 | }); |
| 125 | |
| 126 | document.querySelectorAll("form[data-confirm]").forEach((form) => { |
| 127 | form.addEventListener("submit", (event) => { |
| 128 | if (!window.confirm(form.dataset.confirm)) event.preventDefault(); |
| 129 | }); |
| 130 | }); |
| 131 | |