Skip to content

Commit

Permalink
Support rerendering the toolbar on HTMX boosted pages. (#1686)
Browse files Browse the repository at this point in the history
* Support rerendering the toolbar on HTMX boosted pages.

This reworks the JavaScript integration to put most event handlers on document.body.
This means we'll have slightly slower performance, but it's easier
to handle re-rendering the toolbar when the DOM has been replaced.

* Improve docs' JavaScript usage and links for django std lib docs.

* Make getDebugElement a private method and improve docs.

* Switch internal JavaScript functions and members to camelCase.

This leaves the public API with two snake case functions, show_toolbar and
hide_toolbar.
  • Loading branch information
tim-schilling committed Oct 23, 2022
1 parent 0cff109 commit 7a8920c
Show file tree
Hide file tree
Showing 8 changed files with 151 additions and 66 deletions.
1 change: 0 additions & 1 deletion debug_toolbar/static/debug_toolbar/js/timer.js
@@ -1,7 +1,6 @@
import { $$ } from "./utils.js";

function insertBrowserTiming() {
console.log(["inserted"]);
const timingOffset = performance.timing.navigationStart,
timingEnd = performance.timing.loadEventEnd,
totalTime = timingEnd - timingOffset;
Expand Down
141 changes: 78 additions & 63 deletions debug_toolbar/static/debug_toolbar/js/toolbar.js
Expand Up @@ -2,19 +2,26 @@ import { $$, ajax, replaceToolbarState, debounce } from "./utils.js";

function onKeyDown(event) {
if (event.keyCode === 27) {
djdt.hide_one_level();
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,
init() {
const djDebug = document.getElementById("djDebug");
$$.show(djDebug);
$$.on(
document.getElementById("djDebugPanelList"),
document.body,
"click",
"li a",
"#djDebugPanelList li a",
function (event) {
event.preventDefault();
if (!this.className) {
Expand All @@ -23,23 +30,24 @@ const djdt = {
const panelId = this.className;
const current = document.getElementById(panelId);
if ($$.visible(current)) {
djdt.hide_panels();
djdt.hidePanels();
} else {
djdt.hide_panels();
djdt.hidePanels();

$$.show(current);
this.parentElement.classList.add("djdt-active");

const djDebug = getDebugElement();
const inner = current.querySelector(
".djDebugPanelContent .djdt-scroll"
),
store_id = djDebug.dataset.storeId;
if (store_id && inner.children.length === 0) {
storeId = djDebug.dataset.storeId;
if (storeId && inner.children.length === 0) {
const url = new URL(
djDebug.dataset.renderPanelUrl,
window.location
);
url.searchParams.append("store_id", store_id);
url.searchParams.append("store_id", storeId);
url.searchParams.append("panel_id", panelId);
ajax(url).then(function (data) {
inner.previousElementSibling.remove(); // Remove AJAX loader
Expand All @@ -62,13 +70,13 @@ const djdt = {
}
}
);
$$.on(djDebug, "click", ".djDebugClose", function () {
djdt.hide_one_level();
$$.on(document.body, "click", "#djDebug .djDebugClose", function () {
djdt.hideOneLevel();
});
$$.on(
djDebug,
document.body,
"click",
".djDebugPanelButton input[type=checkbox]",
"#djDebug .djDebugPanelButton input[type=checkbox]",
function () {
djdt.cookie.set(
this.dataset.cookie,
Expand All @@ -82,51 +90,51 @@ const djdt = {
);

// Used by the SQL and template panels
$$.on(djDebug, "click", ".remoteCall", function (event) {
$$.on(document.body, "click", "#djDebug .remoteCall", function (event) {
event.preventDefault();

let url;
const ajax_data = {};
const ajaxData = {};

if (this.tagName === "BUTTON") {
const form = this.closest("form");
url = this.formAction;
ajax_data.method = form.method.toUpperCase();
ajax_data.body = new FormData(form);
ajaxData.method = form.method.toUpperCase();
ajaxData.body = new FormData(form);
} else if (this.tagName === "A") {
url = this.href;
}

ajax(url, ajax_data).then(function (data) {
ajax(url, ajaxData).then(function (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 () {
$$.on(document.body, "click", "#djDebug .djToggleSwitch", function () {
const id = this.dataset.toggleId;
const toggleOpen = "+";
const toggleClose = "-";
const open_me = this.textContent === toggleOpen;
const openMe = this.textContent === toggleOpen;
const name = this.dataset.toggleName;
const container = document.getElementById(name + "_" + id);
container
.querySelectorAll(".djDebugCollapsed")
.forEach(function (e) {
$$.toggle(e, open_me);
$$.toggle(e, openMe);
});
container
.querySelectorAll(".djDebugUncollapsed")
.forEach(function (e) {
$$.toggle(e, !open_me);
$$.toggle(e, !openMe);
});
const self = this;
this.closest(".djDebugPanelContent")
.querySelectorAll(".djToggleDetails_" + id)
.forEach(function (e) {
if (open_me) {
if (openMe) {
e.classList.add("djSelected");
e.classList.remove("djUnselected");
self.textContent = toggleClose;
Expand All @@ -142,19 +150,16 @@ const djdt = {
});
});

document
.getElementById("djHideToolBarButton")
.addEventListener("click", function (event) {
event.preventDefault();
djdt.hide_toolbar();
});
document
.getElementById("djShowToolBarButton")
.addEventListener("click", function () {
if (!djdt.handleDragged) {
djdt.show_toolbar();
}
});
$$.on(document.body, "click", "#djHideToolBarButton", function (event) {
event.preventDefault();
djdt.hideToolbar();
});

$$.on(document.body, "click", "#djShowToolBarButton", function () {
if (!djdt.handleDragged) {
djdt.showToolbar();
}
});
let startPageY, baseY;
const handle = document.getElementById("djDebugToolbarHandle");
function onHandleMove(event) {
Expand All @@ -174,14 +179,18 @@ const djdt = {
djdt.handleDragged = true;
}
}
document
.getElementById("djShowToolBarButton")
.addEventListener("mousedown", function (event) {
$$.on(
document.body,
"mousedown",
"#djShowToolBarButton",
function (event) {
event.preventDefault();
startPageY = event.pageY;
baseY = handle.offsetTop - startPageY;
document.addEventListener("mousemove", onHandleMove);
});
}
);

document.addEventListener("mouseup", function (event) {
document.removeEventListener("mousemove", onHandleMove);
if (djdt.handleDragged) {
Expand All @@ -190,22 +199,27 @@ const djdt = {
requestAnimationFrame(function () {
djdt.handleDragged = false;
});
djdt.ensure_handle_visibility();
djdt.ensureHandleVisibility();
}
});
const djDebug = getDebugElement();
// 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.show_toolbar();
djdt.showToolbar();
} else {
djdt.hide_toolbar();
djdt.hideToolbar();
}
if (djDebug.dataset.sidebarUrl !== undefined) {
djdt.update_on_ajax();
djdt.updateOnAjax();
}
},
hide_panels() {
const djDebug = document.getElementById("djDebug");
hidePanels() {
const djDebug = getDebugElement();
$$.hide(document.getElementById("djDebugWindow"));
djDebug.querySelectorAll(".djdt-panelContent").forEach(function (e) {
$$.hide(e);
Expand All @@ -214,7 +228,7 @@ const djdt = {
e.classList.remove("djdt-active");
});
},
ensure_handle_visibility() {
ensureHandleVisibility() {
const handle = document.getElementById("djDebugToolbarHandle");
// set handle position
const handleTop = Math.min(
Expand All @@ -223,47 +237,48 @@ const djdt = {
);
handle.style.top = handleTop + "px";
},
hide_toolbar() {
djdt.hide_panels();
hideToolbar() {
djdt.hidePanels();

$$.hide(document.getElementById("djDebugToolbar"));

const handle = document.getElementById("djDebugToolbarHandle");
$$.show(handle);
djdt.ensure_handle_visibility();
window.addEventListener("resize", djdt.ensure_handle_visibility);
djdt.ensureHandleVisibility();
window.addEventListener("resize", djdt.ensureHandleVisibility);
document.removeEventListener("keydown", onKeyDown);

localStorage.setItem("djdt.show", "false");
},
hide_one_level() {
hideOneLevel() {
const win = document.getElementById("djDebugWindow");
if ($$.visible(win)) {
$$.hide(win);
} else {
const toolbar = document.getElementById("djDebugToolbar");
if (toolbar.querySelector("li.djdt-active")) {
djdt.hide_panels();
djdt.hidePanels();
} else {
djdt.hide_toolbar();
djdt.hideToolbar();
}
}
},
show_toolbar() {
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.ensure_handle_visibility);
window.removeEventListener("resize", djdt.ensureHandleVisibility);
},
update_on_ajax() {
const sidebar_url =
updateOnAjax() {
const sidebarUrl =
document.getElementById("djDebug").dataset.sidebarUrl;
const slowjax = debounce(ajax, 200);

function handleAjaxResponse(storeId) {
storeId = encodeURIComponent(storeId);
const dest = `${sidebar_url}?store_id=${storeId}`;
const dest = `${sidebarUrl}?store_id=${storeId}`;
slowjax(dest).then(function (data) {
replaceToolbarState(storeId, data);
});
Expand Down Expand Up @@ -342,10 +357,10 @@ const djdt = {
},
};
window.djdt = {
show_toolbar: djdt.show_toolbar,
hide_toolbar: djdt.hide_toolbar,
show_toolbar: djdt.showToolbar,
hide_toolbar: djdt.hideToolbar,
init: djdt.init,
close: djdt.hide_one_level,
close: djdt.hideOneLevel,
cookie: djdt.cookie,
};

Expand Down
3 changes: 2 additions & 1 deletion debug_toolbar/static/debug_toolbar/js/utils.js
@@ -1,5 +1,6 @@
const $$ = {
on(root, eventName, selector, fn) {
root.removeEventListener(eventName, fn);
root.addEventListener(eventName, function (event) {
const target = event.target.closest(selector);
if (root.contains(target)) {
Expand Down Expand Up @@ -107,7 +108,7 @@ function ajaxForm(element) {
function replaceToolbarState(newStoreId, data) {
const djDebug = document.getElementById("djDebug");
djDebug.setAttribute("data-store-id", newStoreId);
// Check if response is empty, it could be due to an expired store_id.
// Check if response is empty, it could be due to an expired storeId.
Object.keys(data).forEach(function (panelId) {
const panel = document.getElementById(panelId);
if (panel) {
Expand Down
5 changes: 5 additions & 0 deletions docs/changes.rst
Expand Up @@ -5,6 +5,11 @@ Pending
-------

* Auto-update History panel for JavaScript ``fetch`` requests.
* Support `HTMX boosting <https://htmx.org/docs/#boosting/>`__ and
re-rendering the toolbar after the DOM has been replaced. This reworks
the JavaScript integration to put most event handlers on document.body.
This means we'll have slightly slower performance, but it's easier
to handle re-rendering the toolbar when the DOM has been replaced.


3.7.0 (2022-09-25)
Expand Down
34 changes: 34 additions & 0 deletions docs/installation.rst
Expand Up @@ -199,3 +199,37 @@ The Debug Toolbar currently doesn't support Django Channels or async projects.
If you are using Django channels are having issues getting panels to load,
please review the documentation for the configuration option
:ref:`RENDER_PANELS <RENDER_PANELS>`.


HTMX
^^^^

If you're using `HTMX`_ to `boost a page`_ you will need to add the following
event handler to your code:

.. code-block:: javascript
{% if debug %}
if (typeof window.htmx !== "undefined") {
htmx.on("htmx:afterSettle", function(detail) {
if (
typeof window.djdt !== "undefined"
&& detail.target instanceof HTMLBodyElement
) {
djdt.show_toolbar();
}
});
}
{% endif %}
The use of ``{% if debug %}`` requires
`django.template.context_processors.debug`_ be included in the
``'context_processors'`` option of the `TEMPLATES`_ setting. Django's
default configuration includes this context processor.


.. _HTMX: https://htmx.org/
.. _boost a page: https://htmx.org/docs/#boosting
.. _django.template.context_processors.debug: https://docs.djangoproject.com/en/4.1/ref/templates/api/#django-template-context-processors-debug
.. _TEMPLATES: https://docs.djangoproject.com/en/4.1/ref/settings/#std-setting-TEMPLATES
5 changes: 4 additions & 1 deletion docs/panels.rst
Expand Up @@ -427,7 +427,10 @@ common methods available.

.. js:function:: djdt.show_toolbar

Shows the toolbar.
Shows the toolbar. This can be used to re-render the toolbar when reloading the
entire DOM. For example, then using `HTMX's boosting`_.

.. _HTMX's boosting: https://htmx.org/docs/#boosting

Events
^^^^^^
Expand Down

0 comments on commit 7a8920c

Please sign in to comment.