/* line numbers */ updateLineNumber(location.hash); for (let line of document.querySelectorAll('.line-number')) { line.addEventListener("click", () => updateLineNumber(line.attributes.getNamedItem("id").value)); } function updateLineNumber(id) { if (id && id.startsWith('#')) { id = id.substring(1); } if (!id) { return; } let element = document.getElementById(id); if (element.classList.contains("line-number")) { for (const line of document.querySelectorAll(".line-active")) { line.classList.remove("line-active"); } element.closest('.entry').classList.add('line-active'); } } /* Scroll to top/bottom buttons */ const downButton = document.getElementById("down-button"); if (downButton) { downButton.addEventListener("click", () => scrollToHeight(document.body.scrollHeight)); } const upButton = document.getElementById("up-button"); if (upButton) { upButton.addEventListener("click", () => scrollToHeight(0)); } /** * Scroll to a specific height * Disable smooth scrolling for large pages * @param {number} top height to scroll to * @param {number} [smoothScrollLimit] only use smooth scrolling if the distance is less than this value */ function scrollToHeight(top, smoothScrollLimit = 10000) { const distance = Math.abs(document.documentElement.scrollTop - top); const behavior = (distance < smoothScrollLimit) ? "smooth" : "instant"; window.scrollTo({left: 0, top, behavior}); } /* * Error collapse toggle. * * Smart fold: every entry within ±ERROR_WINDOW_SIZE of an error stays * visible; runs of non-error entries with no nearby errors collapse into * a draggable fold bar. Vertical drag on the bar progressively reveals or * re-hides lines; a click without drag reveals the next DEFAULT_CLICK_REVEAL * lines. */ const ERROR_WINDOW_SIZE = 25; const PIXELS_PER_LINE_DRAG = 6; const DEFAULT_CLICK_REVEAL = 25; const DRAG_PIXEL_THRESHOLD = 3; const toggleErrorsButton = document.getElementById("error-toggle"); if (toggleErrorsButton) { toggleErrorsButton.addEventListener("click", toggleErrors); } function toggleErrors() { if (toggleErrorsButton.classList.contains("toggled")) { toggleErrorsButton.classList.remove("toggled"); uncollapseAllErrors(); } else { toggleErrorsButton.classList.add("toggled"); collapseAllErrors(); } } function collapseAllErrors() { const lines = Array.from(document.querySelectorAll('.log-inner > .entry')); const total = lines.length; if (!total) return; // Pass 1: mark entries within ±ERROR_WINDOW_SIZE of any error. const mustShow = new Array(total).fill(false); for (let i = 0; i < total; i++) { if (lines[i].classList.contains("entry-error")) { const lo = Math.max(0, i - ERROR_WINDOW_SIZE); const hi = Math.min(total - 1, i + ERROR_WINDOW_SIZE); for (let j = lo; j <= hi; j++) mustShow[j] = true; } } // Pass 2: hide unmarked entries; emit a fold bar at the START of each run. let runEntries = []; const flushRun = () => { if (!runEntries.length) return; const bar = createFoldBar(runEntries); runEntries[0].insertAdjacentElement("beforebegin", bar); runEntries = []; }; for (let i = 0; i < total; i++) { if (!mustShow[i]) { lines[i].style.display = "none"; runEntries.push(lines[i]); } else { flushRun(); } } flushRun(); } function uncollapseAllErrors() { document.querySelectorAll(".log-inner > .entry").forEach(line => line.style.removeProperty("display")); document.querySelectorAll(".collapsed-lines").forEach(bar => bar.remove()); } function createFoldBar(entries) { const bar = document.createElement("div"); bar.classList.add("collapsed-lines", "collapsed-lines-foldable"); bar.appendChild(document.createElement("div")); // line-number column slot const count = document.createElement("div"); count.classList.add("collapsed-lines-count"); bar.appendChild(count); bar._hiddenEntries = entries; bar._revealedCount = 0; renderFoldLabel(bar); attachFoldDragHandler(bar); return bar; } function renderFoldLabel(bar) { const entries = bar._hiddenEntries; const total = entries.length; const revealed = bar._revealedCount; const remaining = total - revealed; const count = bar.querySelector(".collapsed-lines-count"); count.replaceChildren(); if (remaining <= 0) return; const firstHiddenLine = parseInt(entries[revealed].querySelector(".line-number").innerHTML, 10); const lastHiddenLine = parseInt(entries[total - 1].querySelector(".line-number").innerHTML, 10); const word = remaining === 1 ? "line" : "lines"; const grip = document.createElement("i"); grip.className = "fa-solid fa-grip-lines"; count.appendChild(grip); count.append(` ${remaining} ${word} hidden · ${firstHiddenLine}–${lastHiddenLine} · drag to reveal `); count.appendChild(grip.cloneNode()); } function applyFoldReveal(bar, n) { const entries = bar._hiddenEntries; n = Math.max(0, Math.min(entries.length, n)); if (n === bar._revealedCount) return; for (let i = 0; i < entries.length; i++) { if (i < n) entries[i].style.removeProperty("display"); else entries[i].style.display = "none"; } bar._revealedCount = n; renderFoldLabel(bar); } function attachFoldDragHandler(bar) { let dragging = false; let dragStartY = 0; let dragStartReveal = 0; let didDrag = false; bar.addEventListener("pointerdown", (e) => { if (e.button !== 0) return; dragging = true; didDrag = false; dragStartY = e.clientY; dragStartReveal = bar._revealedCount; bar.setPointerCapture(e.pointerId); bar.classList.add("collapsed-lines-dragging"); e.preventDefault(); }); bar.addEventListener("pointermove", (e) => { if (!dragging) return; const dy = e.clientY - dragStartY; if (Math.abs(dy) >= DRAG_PIXEL_THRESHOLD) didDrag = true; const target = dragStartReveal + Math.round(dy / PIXELS_PER_LINE_DRAG); applyFoldReveal(bar, target); }); const finish = (e) => { if (!dragging) return; dragging = false; try { bar.releasePointerCapture(e.pointerId); } catch {} bar.classList.remove("collapsed-lines-dragging"); if (!didDrag) { // Plain click: reveal the next chunk of lines. applyFoldReveal(bar, bar._revealedCount + DEFAULT_CLICK_REVEAL); } if (bar._revealedCount >= bar._hiddenEntries.length) { bar.remove(); } }; bar.addEventListener("pointerup", finish); bar.addEventListener("pointercancel", finish); } /* convert timestamps */ let timeElements = document.querySelectorAll('[data-time]'); for (const element of timeElements) { const timestamp = parseInt(element.dataset.time); if (isNaN(timestamp)) { continue; } const date = new Date(timestamp * 1000); element.innerHTML = date.toLocaleString(); } /* settings */ const settingCheckboxes = document.querySelectorAll(".setting-checkbox"); settingCheckboxes.forEach(checkbox => checkbox.addEventListener("change", handleSettingChange)); let settingsChannel = null; if (typeof BroadcastChannel !== "undefined") { settingsChannel = new BroadcastChannel("mc-logs-settings"); settingsChannel.onmessage = (e) => { if (e.data.type === "settings-updated") { for (const checkbox of settingCheckboxes) { checkbox.checked = !!e.data.settings[checkbox.dataset.key]; applySetting(checkbox); } } }; } function handleSettingChange(e) { let checkbox = e.target; applySetting(checkbox); saveSettings(); if (settingsChannel) { settingsChannel.postMessage({ type: "settings-updated", settings: getCurrentSettings() }); } } function applySetting(checkbox) { let bodyClass = checkbox.dataset.bodyClass; if (checkbox.checked) { document.body.classList.add(bodyClass); } else { document.body.classList.remove(bodyClass); } switch (checkbox.dataset.key) { case "floatingScrollbar": initFloatingScrollbar(); break; } } function getCurrentSettings() { const data = {}; for (const checkbox of settingCheckboxes) { data[checkbox.dataset.key] = checkbox.checked; } return data; } function saveSettings() { const data = {}; for (const checkbox of settingCheckboxes) { data[checkbox.dataset.key] = checkbox.checked; } document.cookie = "IBLOGS_SETTINGS=" + encodeURIComponent(JSON.stringify(data)) + ";path=/;expires=" + new Date(new Date().getTime() + 100 * 365 * 24 * 60 * 60 * 1000).toUTCString(); } /* copy to clipboard */ const copyButtons = document.querySelectorAll("[data-clipboard]"); copyButtons.forEach(button => button.addEventListener("click", handleCopyButtonClick)); const doneClassName = "fa-solid fa-check"; async function handleCopyButtonClick(e) { const button = e.currentTarget; const data = button.dataset.clipboard; await navigator.clipboard.writeText(data); const iconElement = button.querySelector("i"); if (!iconElement) { return; } const originalClassName = iconElement.className; if (originalClassName === doneClassName) { return; } iconElement.className = doneClassName; setTimeout(() => { iconElement.className = originalClassName; }, 2000); } /* delete button */ const deleteButton = document.querySelector(".delete-log-button"); const deleteErrorElement = document.querySelector(".delete-overlay .popover-error"); if (deleteButton) { deleteButton.addEventListener("click", handleDeleteButtonClick); } async function handleDeleteButtonClick() { deleteErrorElement.style.display = "none"; const response = await fetch(window.location.href, { method: "DELETE", credentials: "include" }); if (!response.ok) { deleteErrorElement.style.display = "block"; deleteErrorElement.textContent = `${response.status} (${response.statusText})`; return; } window.location.href = "/"; } /* floating scroll bar */ const browser = getComputedStyle(document.body) .getPropertyValue("--browser") .replaceAll(/['"]/g, '') .trim() .toLowerCase(); const floatingScrollbar = document.querySelector(".floating-scrollbar"); let logContainer = null; if (browser === "firefox") { logContainer = document.querySelector(".log"); } else { logContainer = document.querySelector(".log-inner"); } if (floatingScrollbar && logContainer) { updateFloatingScrollbarWidths(); floatingScrollbar.addEventListener("scroll", () => { syncScroll(floatingScrollbar, logContainer); }); logContainer.addEventListener("scroll", () => { syncScroll(logContainer, floatingScrollbar); }); const observer = new ResizeObserver(() => { updateFloatingScrollbarWidths(); }); observer.observe(logContainer); } function syncScroll(source, target) { if (Math.abs(source.scrollLeft - target.scrollLeft) > 1) { target.scrollLeft = source.scrollLeft; } } function initFloatingScrollbar() { if (!floatingScrollbar || !logContainer) { return; } updateFloatingScrollbarWidths(); syncScroll(logContainer, floatingScrollbar); } function updateFloatingScrollbarWidths() { floatingScrollbar.style.setProperty( "--floating-scrollbar-width", `${logContainer.clientWidth}px` ); floatingScrollbar.style.setProperty( "--floating-scrollbar-content-width", `${logContainer.scrollWidth}px` ); }