import { $$, ajax, debounce, replaceToolbarState } from "./utils.js"; function onKeyDown(event) { if (event.keyCode === 27) { djdt.hideOneLevel(); } } function getDebugElement() { // Fetch the debug element from the DOM. // This is used to avoid writing the element's id // everywhere the element is being selected. A fixed reference // to the element should be avoided because the entire DOM could // be reloaded such as via HTMX boosting. return document.getElementById("djDebug"); } const djdt = { handleDragged: false, needUpdateOnFetch: false, init() { const djDebug = getDebugElement(); djdt.needUpdateOnFetch = djDebug.dataset.updateOnFetch === "True"; $$.on(djDebug, "click", "#djDebugPanelList li a", function (event) { event.preventDefault(); if (!this.className) { return; } const panelId = this.className; const current = document.getElementById(panelId); if ($$.visible(current)) { djdt.hidePanels(); } else { djdt.hidePanels(); $$.show(current); this.parentElement.classList.add("djdt-active"); const inner = current.querySelector( ".djDebugPanelContent .djdt-scroll" ); const requestId = djDebug.dataset.requestId; if (requestId && inner.children.length === 0) { const url = new URL( djDebug.dataset.renderPanelUrl, window.location ); url.searchParams.append("request_id", requestId); url.searchParams.append("panel_id", panelId); ajax(url).then((data) => { inner.previousElementSibling.remove(); // Remove AJAX loader inner.innerHTML = data.content; $$.executeScripts(data.scripts); $$.applyStyles(inner); djDebug.dispatchEvent( new CustomEvent("djdt.panel.render", { detail: { panelId: panelId }, }) ); }); } else { djDebug.dispatchEvent( new CustomEvent("djdt.panel.render", { detail: { panelId: panelId }, }) ); } } }); $$.on(djDebug, "click", ".djDebugClose", () => { djdt.hideOneLevel(); }); $$.on( djDebug, "click", ".djDebugPanelButton input[type=checkbox]", function () { djdt.cookie.set( this.dataset.cookie, this.checked ? "on" : "off", { path: "/", expires: 10, } ); } ); // Used by the SQL and template panels $$.on(djDebug, "click", ".remoteCall", function (event) { event.preventDefault(); let url; const ajaxData = {}; if (this.tagName === "BUTTON") { const form = this.closest("form"); url = this.formAction; ajaxData.method = form.method.toUpperCase(); ajaxData.body = new FormData(form); } else if (this.tagName === "A") { url = this.href; } ajax(url, ajaxData).then((data) => { const win = document.getElementById("djDebugWindow"); win.innerHTML = data.content; $$.show(win); }); }); // Used by the cache, profiling and SQL panels $$.on(djDebug, "click", ".djToggleSwitch", function () { const id = this.dataset.toggleId; const toggleOpen = "+"; const toggleClose = "-"; const openMe = this.textContent === toggleOpen; const name = this.dataset.toggleName; const container = document.getElementById(`${name}_${id}`); for (const el of container.querySelectorAll(".djDebugCollapsed")) { $$.toggle(el, openMe); } for (const el of container.querySelectorAll( ".djDebugUncollapsed" )) { $$.toggle(el, !openMe); } for (const el of this.closest( ".djDebugPanelContent" ).querySelectorAll(`.djToggleDetails_${id}`)) { if (openMe) { el.classList.add("djSelected"); el.classList.remove("djUnselected"); this.textContent = toggleClose; } else { el.classList.remove("djSelected"); el.classList.add("djUnselected"); this.textContent = toggleOpen; } const switch_ = el.querySelector(".djToggleSwitch"); if (switch_) { switch_.textContent = this.textContent; } } }); $$.on(djDebug, "click", "#djHideToolBarButton", (event) => { event.preventDefault(); djdt.hideToolbar(); }); $$.on(djDebug, "click", "#djShowToolBarButton", () => { if (!djdt.handleDragged) { djdt.showToolbar(); } }); let startPageY; let baseY; const handle = document.getElementById("djDebugToolbarHandle"); function onHandleMove(event) { // Chrome can send spurious mousemove events, so don't do anything unless the // cursor really moved. Otherwise, it will be impossible to expand the toolbar // due to djdt.handleDragged being set to true. if (djdt.handleDragged || event.pageY !== startPageY) { let top = baseY + event.pageY; if (top < 0) { top = 0; } else if (top + handle.offsetHeight > window.innerHeight) { top = window.innerHeight - handle.offsetHeight; } handle.style.top = `${top}px`; djdt.handleDragged = true; } } $$.on(djDebug, "mousedown", "#djShowToolBarButton", (event) => { event.preventDefault(); startPageY = event.pageY; baseY = handle.offsetTop - startPageY; document.addEventListener("mousemove", onHandleMove); document.addEventListener( "mouseup", (event) => { document.removeEventListener("mousemove", onHandleMove); if (djdt.handleDragged) { event.preventDefault(); localStorage.setItem("djdt.top", handle.offsetTop); requestAnimationFrame(() => { djdt.handleDragged = false; }); djdt.ensureHandleVisibility(); } }, { once: true } ); }); // Make sure the debug element is rendered at least once. // showToolbar will continue to show it in the future if the // entire DOM is reloaded. $$.show(djDebug); const show = localStorage.getItem("djdt.show") || djDebug.dataset.defaultShow; if (show === "true") { djdt.showToolbar(); } else { djdt.hideToolbar(); } if (djDebug.dataset.sidebarUrl !== undefined) { djdt.updateOnAjax(); } const prefersDark = window.matchMedia( "(prefers-color-scheme: dark)" ).matches; const themeList = prefersDark ? ["auto", "light", "dark"] : ["auto", "dark", "light"]; const setTheme = (theme) => { djDebug.setAttribute( "data-theme", theme === "auto" ? (prefersDark ? "dark" : "light") : theme ); djDebug.setAttribute("data-user-theme", theme); }; // Updates the theme using user settings let userTheme = localStorage.getItem("djdt.user-theme") || "auto"; setTheme(userTheme); // Adds the listener to the Theme Toggle Button $$.on(djDebug, "click", "#djToggleThemeButton", () => { const index = themeList.indexOf(userTheme); userTheme = themeList[(index + 1) % themeList.length]; localStorage.setItem("djdt.user-theme", userTheme); setTheme(userTheme); }); }, hidePanels() { const djDebug = getDebugElement(); $$.hide(document.getElementById("djDebugWindow")); for (const el of djDebug.querySelectorAll(".djdt-panelContent")) { $$.hide(el); } for (const el of document.querySelectorAll("#djDebugToolbar li")) { el.classList.remove("djdt-active"); } }, ensureHandleVisibility() { const handle = document.getElementById("djDebugToolbarHandle"); // set handle position const handleTop = Math.min( localStorage.getItem("djdt.top") || 265, window.innerHeight - handle.offsetWidth ); handle.style.top = `${handleTop}px`; }, hideToolbar() { djdt.hidePanels(); $$.hide(document.getElementById("djDebugToolbar")); const handle = document.getElementById("djDebugToolbarHandle"); $$.show(handle); djdt.ensureHandleVisibility(); window.addEventListener("resize", djdt.ensureHandleVisibility); document.removeEventListener("keydown", onKeyDown); localStorage.setItem("djdt.show", "false"); }, hideOneLevel() { const win = document.getElementById("djDebugWindow"); if ($$.visible(win)) { $$.hide(win); } else { const toolbar = document.getElementById("djDebugToolbar"); if (toolbar.querySelector("li.djdt-active")) { djdt.hidePanels(); } else { djdt.hideToolbar(); } } }, showToolbar() { document.addEventListener("keydown", onKeyDown); $$.show(document.getElementById("djDebug")); $$.hide(document.getElementById("djDebugToolbarHandle")); $$.show(document.getElementById("djDebugToolbar")); localStorage.setItem("djdt.show", "true"); window.removeEventListener("resize", djdt.ensureHandleVisibility); }, updateOnAjax() { const sidebarUrl = document.getElementById("djDebug").dataset.sidebarUrl; const slowjax = debounce(ajax, 200); function handleAjaxResponse(requestId) { const encodedRequestId = encodeURIComponent(requestId); const dest = `${sidebarUrl}?request_id=${encodedRequestId}`; slowjax(dest).then((data) => { if (djdt.needUpdateOnFetch) { replaceToolbarState(encodedRequestId, data); } }); } // Patch XHR / traditional AJAX requests const origOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function (...args) { this.addEventListener("load", function () { // Chromium emits a "Refused to get unsafe header" uncatchable warning // when the header can't be fetched. While it doesn't impede execution // it's worrisome to developers. if ( this.getAllResponseHeaders().indexOf("djdt-request-id") >= 0 ) { handleAjaxResponse( this.getResponseHeader("djdt-request-id") ); } }); origOpen.apply(this, args); }; const origFetch = window.fetch; window.fetch = function (...args) { // Heads up! Before modifying this code, please be aware of the // possible unhandled errors that might arise from changing this. // For details, see // https://github.com/django-commons/django-debug-toolbar/pull/2100 const promise = origFetch.apply(this, args); return promise.then((response) => { if (response.headers.get("djdt-request-id") !== null) { try { handleAjaxResponse( response.headers.get("djdt-request-id") ); } catch (err) { throw new Error( `"${err.name}" occurred within django-debug-toolbar: ${err.message}` ); } } return response; }); }; }, cookie: { get(key) { if (!document.cookie.includes(key)) { return null; } const cookieArray = document.cookie.split("; "); const cookies = {}; for (const e of cookieArray) { const parts = e.split("="); cookies[parts[0]] = parts[1]; } return cookies[key]; }, set(key, value, options = {}) { if (typeof options.expires === "number") { const days = options.expires; const expires = new Date(); expires.setDate(expires.setDate() + days); options.expires = expires; } document.cookie = [ `${encodeURIComponent(key)}=${String(value)}`, options.expires ? `; expires=${options.expires.toUTCString()}` : "", options.path ? `; path=${options.path}` : "", options.domain ? `; domain=${options.domain}` : "", options.secure ? "; secure" : "", "samesite" in options ? `; samesite=${options.samesite}` : "; samesite=lax", ].join(""); return value; }, }, }; window.djdt = { show_toolbar: djdt.showToolbar, hide_toolbar: djdt.hideToolbar, init: djdt.init, close: djdt.hideOneLevel, cookie: djdt.cookie, }; if (document.readyState !== "loading") { djdt.init(); } else { document.addEventListener("DOMContentLoaded", djdt.init); }