Add JavaScript functionality for dynamic UI updates and filtering

- Implemented font color configuration based on numeric values in various sections.
- Added resizing functionality for input fields to accommodate text length.
- Initialized filters on document ready for improved user interaction.
- Created visualization for profile data using fetched dot format.
- Enhanced SQL detail page with click event handling for row navigation.
- Ensured consistent highlighting for code blocks across multiple pages.
This commit is contained in:
pacnpal
2025-08-20 11:33:23 -04:00
parent 37a20f83ba
commit bead0654df
149 changed files with 26860 additions and 5191 deletions

View File

@@ -0,0 +1,108 @@
import { $$, ajaxForm, replaceToolbarState } from "./utils.js";
const djDebug = document.getElementById("djDebug");
function difference(setA, setB) {
const _difference = new Set(setA);
for (const elem of setB) {
_difference.delete(elem);
}
return _difference;
}
/**
* Create an array of dataset properties from a NodeList.
*/
function pluckData(nodes, key) {
return [...nodes].map((obj) => obj.dataset[key]);
}
function refreshHistory() {
const formTarget = djDebug.querySelector(".refreshHistory");
const container = document.getElementById("djdtHistoryRequests");
const oldIds = new Set(
pluckData(
container.querySelectorAll("tr[data-request-id]"),
"requestId"
)
);
ajaxForm(formTarget)
.then((data) => {
// Remove existing rows first then re-populate with new data
for (const node of container.querySelectorAll(
"tr[data-request-id]"
)) {
node.remove();
}
for (const request of data.requests) {
container.innerHTML = request.content + container.innerHTML;
}
})
.then(() => {
const allIds = new Set(
pluckData(
container.querySelectorAll("tr[data-request-id]"),
"requestId"
)
);
const newIds = difference(allIds, oldIds);
const lastRequestId = newIds.values().next().value;
return {
allIds,
newIds,
lastRequestId,
};
})
.then((refreshInfo) => {
for (const newId of refreshInfo.newIds) {
const row = container.querySelector(
`tr[data-request-id="${newId}"]`
);
row.classList.add("flash-new");
}
setTimeout(() => {
for (const row of container.querySelectorAll(
"tr[data-request-id]"
)) {
row.classList.remove("flash-new");
}
}, 2000);
});
}
function switchHistory(newRequestId) {
const formTarget = djDebug.querySelector(
`.switchHistory[data-request-id='${newRequestId}']`
);
const tbody = formTarget.closest("tbody");
const highlighted = tbody.querySelector(".djdt-highlighted");
if (highlighted) {
highlighted.classList.remove("djdt-highlighted");
}
formTarget.closest("tr").classList.add("djdt-highlighted");
ajaxForm(formTarget).then((data) => {
if (Object.keys(data).length === 0) {
const container = document.getElementById("djdtHistoryRequests");
container.querySelector(
`button[data-request-id="${newRequestId}"]`
).innerHTML = "Switch [EXPIRED]";
}
replaceToolbarState(newRequestId, data);
});
}
$$.on(djDebug, "click", ".switchHistory", function (event) {
event.preventDefault();
switchHistory(this.dataset.requestId);
});
$$.on(djDebug, "click", ".refreshHistory", (event) => {
event.preventDefault();
refreshHistory();
});
// We don't refresh the whole toolbar each fetch or ajax request,
// so we need to refresh the history when we open the panel
$$.onPanelRender(djDebug, "HistoryPanel", refreshHistory);

View File

@@ -0,0 +1 @@
document.getElementById("redirect_to").focus();

View File

@@ -0,0 +1,82 @@
import { $$ } from "./utils.js";
function insertBrowserTiming() {
const timingOffset = performance.timing.navigationStart;
const timingEnd = performance.timing.loadEventEnd;
const totalTime = timingEnd - timingOffset;
function getLeft(stat) {
if (totalTime !== 0) {
return (
((performance.timing[stat] - timingOffset) / totalTime) * 100.0
);
}
return 0;
}
function getCSSWidth(stat, endStat) {
let width = 0;
if (totalTime !== 0) {
width =
((performance.timing[endStat] - performance.timing[stat]) /
totalTime) *
100.0;
}
const denominator = 100.0 - getLeft(stat);
if (denominator !== 0) {
// Calculate relative percent (same as sql panel logic)
width = (100.0 * width) / denominator;
} else {
width = 0;
}
return width < 1 ? "2px" : `${width}%`;
}
function addRow(tbody, stat, endStat) {
const row = document.createElement("tr");
const elapsed = performance.timing[stat] - timingOffset;
if (endStat) {
const duration =
performance.timing[endStat] - performance.timing[stat];
// Render a start through end bar
row.innerHTML = `
<td>${stat.replace("Start", "")}</td>
<td><svg class="djDebugLineChart" xmlns="http://www.w3.org/2000/svg" viewbox="0 0 100 5" preserveAspectRatio="none"><rect y="0" height="5" fill="#ccc" /></svg></td>
<td>${elapsed} (+${duration})</td>
`;
row.querySelector("rect").setAttribute(
"width",
getCSSWidth(stat, endStat)
);
} else {
// Render a point in time
row.innerHTML = `
<td>${stat}</td>
<td><svg class="djDebugLineChart" xmlns="http://www.w3.org/2000/svg" viewbox="0 0 100 5" preserveAspectRatio="none"><rect y="0" height="5" fill="#ccc" /></svg></td>
<td>${elapsed}</td>
`;
row.querySelector("rect").setAttribute("width", 2);
}
row.querySelector("rect").setAttribute("x", getLeft(stat));
tbody.appendChild(row);
}
const browserTiming = document.getElementById("djDebugBrowserTiming");
// Determine if the browser timing section has already been rendered.
if (browserTiming.classList.contains("djdt-hidden")) {
const tbody = document.getElementById("djDebugBrowserTimingTableBody");
// This is a reasonably complete and ordered set of timing periods (2 params) and events (1 param)
addRow(tbody, "domainLookupStart", "domainLookupEnd");
addRow(tbody, "connectStart", "connectEnd");
addRow(tbody, "requestStart", "responseEnd"); // There is no requestEnd
addRow(tbody, "responseStart", "responseEnd");
addRow(tbody, "domLoading", "domComplete"); // Spans the events below
addRow(tbody, "domInteractive");
addRow(tbody, "domContentLoadedEventStart", "domContentLoadedEventEnd");
addRow(tbody, "loadEventStart", "loadEventEnd");
browserTiming.classList.remove("djdt-hidden");
}
}
const djDebug = document.getElementById("djDebug");
// Insert the browser timing now since it's possible for this
// script to miss the initial panel load event.
insertBrowserTiming();
$$.onPanelRender(djDebug, "TimerPanel", insertBrowserTiming);

View File

@@ -0,0 +1,403 @@
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);
}

View File

@@ -0,0 +1,144 @@
const $$ = {
on(root, eventName, selector, fn) {
root.removeEventListener(eventName, fn);
root.addEventListener(eventName, (event) => {
const target = event.target.closest(selector);
if (root.contains(target)) {
fn.call(target, event);
}
});
},
onPanelRender(root, panelId, fn) {
/*
This is a helper function to attach a handler for a `djdt.panel.render`
event of a specific panel.
root: The container element that the listener should be attached to.
panelId: The Id of the panel.
fn: A function to execute when the event is triggered.
*/
root.addEventListener("djdt.panel.render", (event) => {
if (event.detail.panelId === panelId) {
fn.call(event);
}
});
},
show(element) {
element.classList.remove("djdt-hidden");
},
hide(element) {
element.classList.add("djdt-hidden");
},
toggle(element, value) {
if (value) {
$$.show(element);
} else {
$$.hide(element);
}
},
visible(element) {
return !element.classList.contains("djdt-hidden");
},
executeScripts(scripts) {
for (const script of scripts) {
const el = document.createElement("script");
el.type = "module";
el.src = script;
el.async = true;
document.head.appendChild(el);
}
},
applyStyles(container) {
/*
* Given a container element, apply styles set via data-djdt-styles attribute.
* The format is data-djdt-styles="styleName1:value;styleName2:value2"
* The style names should use the CSSStyleDeclaration camel cased names.
*/
for (const element of container.querySelectorAll(
"[data-djdt-styles]"
)) {
const styles = element.dataset.djdtStyles || "";
for (const styleText of styles.split(";")) {
const styleKeyPair = styleText.split(":");
if (styleKeyPair.length === 2) {
const name = styleKeyPair[0].trim();
const value = styleKeyPair[1].trim();
element.style[name] = value;
}
}
}
},
};
function ajax(url, init) {
return fetch(url, Object.assign({ credentials: "same-origin" }, init))
.then((response) => {
if (response.ok) {
return response
.json()
.catch((error) =>
Promise.reject(
new Error(
`The response is a invalid Json object : ${error}`
)
)
);
}
return Promise.reject(
new Error(`${response.status}: ${response.statusText}`)
);
})
.catch((error) => {
const win = document.getElementById("djDebugWindow");
win.innerHTML = `<div class="djDebugPanelTitle"><h3>${error.message}</h3><button type="button" class="djDebugClose">»</button></div>`;
$$.show(win);
throw error;
});
}
function ajaxForm(element) {
const form = element.closest("form");
const url = new URL(form.action);
const formData = new FormData(form);
for (const [name, value] of formData.entries()) {
url.searchParams.append(name, value);
}
const ajaxData = {
method: form.method.toUpperCase(),
};
return ajax(url, ajaxData);
}
function replaceToolbarState(newRequestId, data) {
const djDebug = document.getElementById("djDebug");
djDebug.setAttribute("data-request-id", newRequestId);
// Check if response is empty, it could be due to an expired requestId.
for (const panelId of Object.keys(data)) {
const panel = document.getElementById(panelId);
if (panel) {
panel.outerHTML = data[panelId].content;
document.getElementById(`djdt-${panelId}`).outerHTML =
data[panelId].button;
}
}
}
function debounce(func, delay) {
let timer = null;
let resolves = [];
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
const result = func(...args);
for (const r of resolves) {
r(result);
}
resolves = [];
}, delay);
return new Promise((r) => resolves.push(r));
};
}
export { $$, ajax, ajaxForm, replaceToolbarState, debounce };