Skip to content

Commit 2ed4ce6

Browse files
committed
Make DebugKit toolbar robust with JS frameworks like HTMX, Boost
- Persist toolbar iframe across body swaps using a MutationObserver on <body> - Update iframe src with latest X-DEBUGKIT-ID from AJAX responses - Prevent duplicate message event listeners and double XHR proxying - Streamline injection logic for efficiency and reliability
1 parent 976cffe commit 2ed4ce6

File tree

1 file changed

+53
-12
lines changed

1 file changed

+53
-12
lines changed

webroot/js/inject-iframe.js

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,20 @@ if (elem) {
3434
}
3535
};
3636

37-
const onReady = () => {
37+
let observer;
38+
39+
function injectDebugKitIframe() {
3840
if (!win.debugKitId) {
3941
return;
4042
}
43+
4144
const { body } = doc;
4245

43-
// Cannot use css text, because of CSP compatibility.
46+
// Remove the old iframe if it exists (in case of ajax swaps).
47+
if (iframe && iframe.parentNode) {
48+
iframe.parentNode.removeChild(iframe);
49+
}
50+
4451
iframe = doc.createElement('iframe');
4552
iframe.style.position = 'fixed';
4653
iframe.style.bottom = 0;
@@ -51,24 +58,42 @@ if (elem) {
5158
iframe.style.zIndex = 99999;
5259
iframe.height = 40;
5360
iframe.width = 40;
54-
iframe.src = `${window.debugKitBaseUrl}debug-kit/toolbar/${window.debugKitId}`;
55-
61+
iframe.src = `${window.debugKitBaseUrl}debug-kit/toolbar/${window.debugKitId}`;;
5662
body.appendChild(iframe);
5763
bodyOverflow = body.style.overflow;
64+
if (!win.debugKitMessageListenerApplied) {
65+
window.addEventListener('message', onMessage, false);
66+
win.debugKitMessageListenerApplied = true;
67+
}
68+
}
5869

59-
window.addEventListener('message', onMessage, false);
70+
const onReady = () => {
71+
injectDebugKitIframe();
72+
if (!observer) {
73+
observer = new MutationObserver(() => {
74+
if (!doc.body.querySelector('iframe[src*="debug-kit/toolbar/"]')) {
75+
injectDebugKitIframe();
76+
}
77+
});
78+
if (doc.body) {
79+
observer.observe(doc.body, { childList: true });
80+
}
81+
}
6082
};
6183

6284
const logAjaxRequest = (original) => function ajaxRequest() {
6385
if (this.readyState === 4 && this.getResponseHeader('X-DEBUGKIT-ID')) {
86+
const newId = this.getResponseHeader('X-DEBUGKIT-ID');
87+
window.debugKitId = newId;
6488
const params = {
65-
requestId: this.getResponseHeader('X-DEBUGKIT-ID'),
89+
requestId: newId,
6690
status: this.status,
6791
date: new Date(),
6892
method: this._arguments && this._arguments[0],
6993
url: this._arguments && this._arguments[1],
7094
type: this.getResponseHeader('Content-Type'),
7195
};
96+
7297
iframe.contentWindow.postMessage(`ajax-completed$$${JSON.stringify(params)}`, window.location.origin);
7398
}
7499
if (original) {
@@ -78,32 +103,48 @@ if (elem) {
78103
};
79104

80105
const proxyAjaxOpen = () => {
106+
if (window.XMLHttpRequest.prototype._debugKitOpenProxied) {
107+
return;
108+
}
81109
const proxied = window.XMLHttpRequest.prototype.open;
82110
window.XMLHttpRequest.prototype.open = function ajaxCall(...args) {
83111
this._arguments = args;
84112
return proxied.apply(this, [].slice.call(args));
85113
};
114+
window.XMLHttpRequest.prototype._debugKitOpenProxied = true;
86115
};
87116

88117
const proxyAjaxSend = () => {
118+
if (window.XMLHttpRequest.prototype._debugKitSendProxied) {
119+
return;
120+
}
89121
const proxied = window.XMLHttpRequest.prototype.send;
90122
window.XMLHttpRequest.prototype.send = function ajaxCall(...args) {
91123
this.onreadystatechange = logAjaxRequest(this.onreadystatechange);
92124
return proxied.apply(this, [].slice.call(args));
93125
};
126+
window.XMLHttpRequest.prototype._debugKitSendProxied = true;
94127
};
95128

96129
// Bind on ready callbacks to DOMContentLoaded (native js)
97130
// Since the body is already loaded (DOMContentLoaded), the event is not triggered.
98131
if (doc.addEventListener) {
99132
// This ensures that all event listeners get applied only once.
100133
if (!win.debugKitListenersApplied) {
101-
// Add support for turbo DOMContentLoaded alternative
102-
// see https://turbo.hotwired.dev/reference/events#turbo%3Aload
103-
const loadedEvent = typeof Turbo !== 'undefined' && Turbo !== null ? 'turbo:load' : 'DOMContentLoaded';
104-
doc.addEventListener(loadedEvent, onReady, false);
105-
doc.addEventListener(loadedEvent, proxyAjaxOpen, false);
106-
doc.addEventListener(loadedEvent, proxyAjaxSend, false);
134+
doc.addEventListener('DOMContentLoaded', onReady, false);
135+
if (!win.debugKitAjaxProxied) {
136+
doc.addEventListener('DOMContentLoaded', proxyAjaxOpen, false);
137+
doc.addEventListener('DOMContentLoaded', proxyAjaxSend, false);
138+
if (doc.readyState === 'interactive' || doc.readyState === 'complete') {
139+
proxyAjaxOpen();
140+
proxyAjaxSend();
141+
}
142+
win.debugKitAjaxProxied = true;
143+
}
144+
doc.addEventListener('DOMContentLoaded', onReady, false);
145+
if (doc.readyState === 'interactive' || doc.readyState === 'complete') {
146+
onReady();
147+
}
107148
win.debugKitListenersApplied = true;
108149
}
109150
} else {

0 commit comments

Comments
 (0)