Skip to content

Commit a9c6988

Browse files
Use shadow dom to render the toolbar (#2266)
Co-authored-by: Tim Schilling <schillingt@better-simple.com>
1 parent 8605422 commit a9c6988

13 files changed

Lines changed: 139 additions & 92 deletions

File tree

debug_toolbar/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def _is_running_tests():
3232
"ROOT_TAG_EXTRA_ATTRS": "",
3333
"SHOW_COLLAPSED": False,
3434
"SHOW_TOOLBAR_CALLBACK": "debug_toolbar.middleware.show_toolbar",
35+
"USE_SHADOW_DOM": True,
3536
"TOOLBAR_LANGUAGE": None,
3637
"TOOLBAR_STORE_CLASS": "debug_toolbar.store.MemoryStore",
3738
"UPDATE_ON_FETCH": False,

debug_toolbar/static/debug_toolbar/css/toolbar.css

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* Variable definitions */
2-
:root {
2+
#djDebug {
33
/* Font families are the same as in Django admin/css/base.css */
44
--djdt-font-family-primary:
55
"Segoe UI", system-ui, Roboto, "Helvetica Neue", Arial, sans-serif,
@@ -13,7 +13,7 @@
1313
"Noto Color Emoji";
1414
}
1515

16-
:root,
16+
#djDebug,
1717
#djDebug[data-theme="light"] {
1818
--djdt-font-color: black;
1919
--djdt-background-color: white;

debug_toolbar/static/debug_toolbar/js/history.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { $$, ajaxForm, replaceToolbarState } from "./utils.js";
1+
import { $$, ajaxForm, getDebugElement, replaceToolbarState } from "./utils.js";
22

3-
const djDebug = document.getElementById("djDebug");
3+
const djDebug = getDebugElement();
44

55
function difference(setA, setB) {
66
const _difference = new Set(setA);
@@ -19,7 +19,7 @@ function pluckData(nodes, key) {
1919

2020
function refreshHistory() {
2121
const formTarget = djDebug.querySelector(".refreshHistory");
22-
const container = document.getElementById("djdtHistoryRequests");
22+
const container = djDebug.querySelector("#djdtHistoryRequests");
2323
const oldIds = new Set(
2424
pluckData(
2525
container.querySelectorAll("tr[data-request-id]"),
@@ -85,7 +85,7 @@ function switchHistory(newRequestId) {
8585

8686
ajaxForm(formTarget).then((data) => {
8787
if (Object.keys(data).length === 0) {
88-
const container = document.getElementById("djdtHistoryRequests");
88+
const container = djDebug.querySelector("#djdtHistoryRequests");
8989
container.querySelector(
9090
`button[data-request-id="${newRequestId}"]`
9191
).innerHTML = "Switch [EXPIRED]";

debug_toolbar/static/debug_toolbar/js/timer.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { $$ } from "./utils.js";
1+
import { $$, getDebugElement } from "./utils.js";
2+
3+
const djDebug = getDebugElement();
24

35
function insertBrowserTiming() {
46
const timingOffset = performance.timing.navigationStart;
@@ -58,10 +60,10 @@ function insertBrowserTiming() {
5860
tbody.appendChild(row);
5961
}
6062

61-
const browserTiming = document.getElementById("djDebugBrowserTiming");
63+
const browserTiming = djDebug.querySelector("#djDebugBrowserTiming");
6264
// Determine if the browser timing section has already been rendered.
6365
if (browserTiming.classList.contains("djdt-hidden")) {
64-
const tbody = document.getElementById("djDebugBrowserTimingTableBody");
66+
const tbody = djDebug.querySelector("#djDebugBrowserTimingTableBody");
6567
// This is a reasonably complete and ordered set of timing periods (2 params) and events (1 param)
6668
addRow(tbody, "domainLookupStart", "domainLookupEnd");
6769
addRow(tbody, "connectStart", "connectEnd");
@@ -75,7 +77,6 @@ function insertBrowserTiming() {
7577
}
7678
}
7779

78-
const djDebug = document.getElementById("djDebug");
7980
// Insert the browser timing now since it's possible for this
8081
// script to miss the initial panel load event.
8182
insertBrowserTiming();

debug_toolbar/static/debug_toolbar/js/toolbar.js

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,17 @@
1-
import { $$, ajax, debounce, replaceToolbarState } from "./utils.js";
1+
import {
2+
$$,
3+
ajax,
4+
debounce,
5+
getDebugElement,
6+
replaceToolbarState,
7+
} from "./utils.js";
28

39
function onKeyDown(event) {
410
if (event.keyCode === 27) {
511
djdt.hideOneLevel();
612
}
713
}
814

9-
function getDebugElement() {
10-
// Fetch the debug element from the DOM.
11-
// This is used to avoid writing the element's id
12-
// everywhere the element is being selected. A fixed reference
13-
// to the element should be avoided because the entire DOM could
14-
// be reloaded such as via HTMX boosting.
15-
return document.getElementById("djDebug");
16-
}
17-
1815
const djdt = {
1916
handleDragged: false,
2017
needUpdateOnFetch: false,
@@ -27,7 +24,7 @@ const djdt = {
2724
return;
2825
}
2926
const panelId = this.className;
30-
const current = document.getElementById(panelId);
27+
const current = djDebug.querySelector(`#${panelId}`);
3128
if ($$.visible(current)) {
3229
djdt.hidePanels();
3330
} else {
@@ -103,7 +100,7 @@ const djdt = {
103100
}
104101

105102
ajax(url, ajaxData).then((data) => {
106-
const win = document.getElementById("djDebugWindow");
103+
const win = djDebug.querySelector("#djDebugWindow");
107104
win.innerHTML = data.content;
108105
$$.show(win);
109106
});
@@ -116,7 +113,7 @@ const djdt = {
116113
const toggleClose = "-";
117114
const openMe = this.textContent === toggleOpen;
118115
const name = this.dataset.toggleName;
119-
const container = document.getElementById(`${name}_${id}`);
116+
const container = djDebug.querySelector(`#${name}_${id}`);
120117
for (const el of container.querySelectorAll(".djDebugCollapsed")) {
121118
$$.toggle(el, openMe);
122119
}
@@ -156,7 +153,7 @@ const djdt = {
156153
});
157154
let startPageY;
158155
let baseY;
159-
const handle = document.getElementById("djDebugToolbarHandle");
156+
const handle = djDebug.querySelector("#djDebugToolbarHandle");
160157
function onHandleMove(event) {
161158
// Chrome can send spurious mousemove events, so don't do anything unless the
162159
// cursor really moved. Otherwise, it will be impossible to expand the toolbar
@@ -240,16 +237,17 @@ const djdt = {
240237
},
241238
hidePanels() {
242239
const djDebug = getDebugElement();
243-
$$.hide(document.getElementById("djDebugWindow"));
240+
$$.hide(djDebug.querySelector("#djDebugWindow"));
244241
for (const el of djDebug.querySelectorAll(".djdt-panelContent")) {
245242
$$.hide(el);
246243
}
247-
for (const el of document.querySelectorAll("#djDebugToolbar li")) {
244+
for (const el of djDebug.querySelectorAll("#djDebugToolbar li")) {
248245
el.classList.remove("djdt-active");
249246
}
250247
},
251248
ensureHandleVisibility() {
252-
const handle = document.getElementById("djDebugToolbarHandle");
249+
const djDebug = getDebugElement();
250+
const handle = djDebug.querySelector("#djDebugToolbarHandle");
253251
// set handle position
254252
const handleTop = Math.min(
255253
localStorage.getItem("djdt.top") || 265,
@@ -258,11 +256,12 @@ const djdt = {
258256
handle.style.top = `${handleTop}px`;
259257
},
260258
hideToolbar() {
259+
const djDebug = getDebugElement();
261260
djdt.hidePanels();
262261

263-
$$.hide(document.getElementById("djDebugToolbar"));
262+
$$.hide(djDebug.querySelector("#djDebugToolbar"));
264263

265-
const handle = document.getElementById("djDebugToolbarHandle");
264+
const handle = djDebug.querySelector("#djDebugToolbarHandle");
266265
$$.show(handle);
267266
djdt.ensureHandleVisibility();
268267
window.addEventListener("resize", djdt.ensureHandleVisibility);
@@ -271,11 +270,12 @@ const djdt = {
271270
localStorage.setItem("djdt.show", "false");
272271
},
273272
hideOneLevel() {
274-
const win = document.getElementById("djDebugWindow");
273+
const djDebug = getDebugElement();
274+
const win = djDebug.querySelector("#djDebugWindow");
275275
if ($$.visible(win)) {
276276
$$.hide(win);
277277
} else {
278-
const toolbar = document.getElementById("djDebugToolbar");
278+
const toolbar = djDebug.querySelector("#djDebugToolbar");
279279
if (toolbar.querySelector("li.djdt-active")) {
280280
djdt.hidePanels();
281281
} else {
@@ -284,17 +284,17 @@ const djdt = {
284284
}
285285
},
286286
showToolbar() {
287+
const djDebug = getDebugElement();
287288
document.addEventListener("keydown", onKeyDown);
288-
$$.show(document.getElementById("djDebug"));
289-
$$.hide(document.getElementById("djDebugToolbarHandle"));
290-
$$.show(document.getElementById("djDebugToolbar"));
289+
$$.show(djDebug);
290+
$$.hide(djDebug.querySelector("#djDebugToolbarHandle"));
291+
$$.show(djDebug.querySelector("#djDebugToolbar"));
291292
localStorage.setItem("djdt.show", "true");
292293
window.removeEventListener("resize", djdt.ensureHandleVisibility);
293294
},
294295
updateOnAjax() {
295296
const handleAjaxResponse = debounce(async (requestId) => {
296-
const sidebarUrl =
297-
document.getElementById("djDebug").dataset.sidebarUrl;
297+
const sidebarUrl = getDebugElement().dataset.sidebarUrl;
298298

299299
const encodedRequestId = encodeURIComponent(requestId);
300300
const dest = `${sidebarUrl}?request_id=${encodedRequestId}`;

debug_toolbar/static/debug_toolbar/js/utils.js

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,21 @@ const $$ = {
7070
},
7171
};
7272

73+
/**
74+
* Fetch the debug element from the DOM.
75+
*
76+
* This is used to avoid writing the element's id everywhere the element
77+
* is being selected. A fixed reference to the element should be avoided
78+
* because the entire DOM could be reloaded such as via HTMX boosting.
79+
*/
80+
function getDebugElement() {
81+
let root = document.getElementById("djDebugRoot");
82+
if (root.shadowRoot) {
83+
root = root.shadowRoot;
84+
}
85+
return root.querySelector("#djDebug");
86+
}
87+
7388
function ajax(url, init) {
7489
return fetch(url, Object.assign({ credentials: "same-origin" }, init))
7590
.then((response) => {
@@ -89,7 +104,8 @@ function ajax(url, init) {
89104
);
90105
})
91106
.catch((error) => {
92-
const win = document.getElementById("djDebugWindow");
107+
const djDebug = getDebugElement();
108+
const win = djDebug.querySelector("#djDebugWindow");
93109
win.innerHTML = `<div class="djDebugPanelTitle"><h3>${error.message}</h3><button type="button" class="djDebugClose">»</button></div>`;
94110
$$.show(win);
95111
throw error;
@@ -110,14 +126,14 @@ function ajaxForm(element) {
110126
}
111127

112128
function replaceToolbarState(newRequestId, data) {
113-
const djDebug = document.getElementById("djDebug");
129+
const djDebug = getDebugElement();
114130
djDebug.setAttribute("data-request-id", newRequestId);
115131
// Check if response is empty, it could be due to an expired requestId.
116132
for (const panelId of Object.keys(data)) {
117-
const panel = document.getElementById(panelId);
133+
const panel = djDebug.querySelector(`#${panelId}`);
118134
if (panel) {
119135
panel.outerHTML = data[panelId].content;
120-
document.getElementById(`djdt-${panelId}`).outerHTML =
136+
djDebug.querySelector(`#djdt-${panelId}`).outerHTML =
121137
data[panelId].button;
122138
}
123139
}
@@ -140,4 +156,4 @@ export function debounce(func, timeout) {
140156
};
141157
}
142158

143-
export { $$, ajax, ajaxForm, replaceToolbarState };
159+
export { $$, ajax, ajaxForm, getDebugElement, replaceToolbarState };
Lines changed: 41 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,46 @@
11
{% load i18n static %}
2-
{% block css %}
3-
<link{% if toolbar.csp_nonce %} nonce="{{ toolbar.csp_nonce }}"{% endif %} rel="stylesheet" href="{% static 'debug_toolbar/css/print.css' %}" media="print">
4-
<link{% if toolbar.csp_nonce %} nonce="{{ toolbar.csp_nonce }}"{% endif %} rel="stylesheet" href="{% static 'debug_toolbar/css/toolbar.css' %}">
5-
{% endblock css %}
6-
{% block js %}
7-
<script{% if toolbar.csp_nonce %} nonce="{{ toolbar.csp_nonce }}"{% endif %} type="module" src="{% static 'debug_toolbar/js/toolbar.js' %}" async></script>
8-
{% endblock js %}
9-
<div id="djDebug" class="djdt-hidden" dir="ltr"
10-
{% if not toolbar.should_render_panels %}
11-
data-request-id="{{ toolbar.request_id }}"
12-
data-render-panel-url="{% url 'djdt:render_panel' %}"
13-
{% endif %}
14-
{% url 'djdt:history_sidebar' as history_url %}
15-
{% if history_url %}
16-
data-sidebar-url="{{ history_url }}"
17-
{% endif %}
18-
data-default-show="{% if toolbar.config.SHOW_COLLAPSED %}false{% else %}true{% endif %}"
19-
{{ toolbar.config.ROOT_TAG_EXTRA_ATTRS|safe }} data-update-on-fetch="{{ toolbar.config.UPDATE_ON_FETCH }}">
20-
<div class="djdt-hidden" id="djDebugToolbar">
21-
<ul id="djDebugPanelList">
22-
<li><a id="djHideToolBarButton" href="#" title="{% translate 'Hide toolbar' %}">{% translate "Hide" %} »</a></li>
23-
<li>
24-
<a id="djToggleThemeButton" href="#" title="{% translate 'Toggle Theme' %}">
25-
{% translate "Toggle Theme" %} {% include "debug_toolbar/includes/theme_selector.html" %}
26-
</a>
27-
</li>
2+
<div id="djDebugRoot" {{ toolbar.config.ROOT_TAG_EXTRA_ATTRS|safe }}>
3+
{% if use_shadow_dom %}<template shadowrootmode="open">{% endif %}
4+
{% block css %}
5+
<link{% if toolbar.csp_nonce %} nonce="{{ toolbar.csp_nonce }}"{% endif %} rel="stylesheet" href="{% static 'debug_toolbar/css/print.css' %}" media="print">
6+
<link{% if toolbar.csp_nonce %} nonce="{{ toolbar.csp_nonce }}"{% endif %} rel="stylesheet" href="{% static 'debug_toolbar/css/toolbar.css' %}">
7+
{% endblock css %}
8+
{% block js %}
9+
<script{% if toolbar.csp_nonce %} nonce="{{ toolbar.csp_nonce }}"{% endif %} type="module" src="{% static 'debug_toolbar/js/toolbar.js' %}" async></script>
10+
{% endblock js %}
11+
<div id="djDebug" class="djdt-hidden" dir="ltr"
12+
{% if not toolbar.should_render_panels %}
13+
data-request-id="{{ toolbar.request_id }}"
14+
data-render-panel-url="{% url 'djdt:render_panel' %}"
15+
{% endif %}
16+
{% url 'djdt:history_sidebar' as history_url %}
17+
{% if history_url %}
18+
data-sidebar-url="{{ history_url }}"
19+
{% endif %}
20+
data-default-show="{% if toolbar.config.SHOW_COLLAPSED %}false{% else %}true{% endif %}"
21+
data-update-on-fetch="{{ toolbar.config.UPDATE_ON_FETCH }}">
22+
<div class="djdt-hidden" id="djDebugToolbarHandle">
23+
<div title="{% translate 'Show toolbar' %}" id="djShowToolBarButton">
24+
<span id="djShowToolBarD">D</span><span id="djShowToolBarJ">J</span>DT
25+
</div>
26+
</div>
27+
<div class="djdt-hidden" id="djDebugToolbar">
28+
<ul id="djDebugPanelList">
29+
<li><a id="djHideToolBarButton" href="#" title="{% translate 'Hide toolbar' %}">{% translate "Hide" %} »</a></li>
30+
<li>
31+
<a id="djToggleThemeButton" href="#" title="{% translate 'Toggle Theme' %}">
32+
{% translate "Toggle Theme" %} {% include "debug_toolbar/includes/theme_selector.html" %}
33+
</a>
34+
</li>
35+
{% for panel in toolbar.panels %}
36+
{% include "debug_toolbar/includes/panel_button.html" %}
37+
{% endfor %}
38+
</ul>
39+
</div>
2840
{% for panel in toolbar.panels %}
29-
{% include "debug_toolbar/includes/panel_button.html" %}
41+
{% include "debug_toolbar/includes/panel_content.html" %}
3042
{% endfor %}
31-
</ul>
32-
</div>
33-
<div class="djdt-hidden" id="djDebugToolbarHandle">
34-
<div title="{% translate 'Show toolbar' %}" id="djShowToolBarButton">
35-
<span id="djShowToolBarD">D</span><span id="djShowToolBarJ">J</span>DT
43+
<div id="djDebugWindow" class="djdt-panelContent djdt-hidden"></div>
3644
</div>
37-
</div>
38-
39-
{% for panel in toolbar.panels %}
40-
{% include "debug_toolbar/includes/panel_content.html" %}
41-
{% endfor %}
42-
<div id="djDebugWindow" class="djdt-panelContent djdt-hidden"></div>
45+
{% if use_shadow_dom %}</template>{% endif %}
4346
</div>

debug_toolbar/toolbar.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,10 @@ def render_toolbar(self) -> str:
9797
if not self.should_render_panels():
9898
self.init_store()
9999
try:
100-
context = {"toolbar": self}
100+
context = {
101+
"toolbar": self,
102+
"use_shadow_dom": self.config["USE_SHADOW_DOM"],
103+
}
101104
lang = self.config["TOOLBAR_LANGUAGE"] or get_language()
102105
with lang_override(lang):
103106
return render_to_string("debug_toolbar/base.html", context)

docs/changes.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ Pending
99
* Added a note to the prerequisites section of the installation docs
1010
about requiring an up-to-date browser.
1111
* Dropped support for Django 4.2 and Django 5.1 .
12+
* Updated to render the toolbar in a shadow DOM for better isolation
13+
from the rest of the page. This can be disabled with the setting
14+
``USE_SHADOW_DOM``.
15+
* Note that custom themes overriding CSS variables on :root must move
16+
those overrides to ``#djDebug``, and custom panels that rely on external
17+
styles or DOM lookups reaching into the toolbar will need updates to
18+
work with the shadow DOM.
1219

1320
6.3.0 (2026-04-01)
1421
------------------

0 commit comments

Comments
 (0)