2026-03-24 20:49:22 +01:00
|
|
|
/* Error Debug Page — interactive behavior (inlined by ErrorRenderer) */
|
|
|
|
|
|
2026-04-20 23:01:54 +02:00
|
|
|
function initErrorDebug(root = document) {
|
|
|
|
|
if (!root || typeof root.querySelectorAll !== 'function') {
|
|
|
|
|
return { destroy: () => {} };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const body = root.body;
|
|
|
|
|
if (!(body instanceof HTMLElement)) {
|
|
|
|
|
return { destroy: () => {} };
|
|
|
|
|
}
|
|
|
|
|
if (body.dataset.errorDebugBound === '1') {
|
|
|
|
|
return { destroy: () => {} };
|
|
|
|
|
}
|
|
|
|
|
body.dataset.errorDebugBound = '1';
|
|
|
|
|
|
|
|
|
|
const cleanupFns = [];
|
|
|
|
|
const bind = (target, eventName, handler, options = undefined) => {
|
|
|
|
|
if (!target || typeof target.addEventListener !== 'function') {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
target.addEventListener(eventName, handler, options);
|
|
|
|
|
cleanupFns.push(() => target.removeEventListener(eventName, handler, options));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const tabs = Array.from(root.querySelectorAll('.app-error-debug-tab'));
|
|
|
|
|
const panels = Array.from(root.querySelectorAll('.app-error-debug-panel'));
|
2026-03-24 20:49:22 +01:00
|
|
|
|
2026-03-24 22:00:14 +01:00
|
|
|
const activateTab = (tab) => {
|
2026-04-20 23:01:54 +02:00
|
|
|
if (!(tab instanceof HTMLElement)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-03-24 22:00:14 +01:00
|
|
|
const target = tab.dataset.panel;
|
2026-04-20 23:01:54 +02:00
|
|
|
tabs.forEach((value) => {
|
|
|
|
|
if (value instanceof HTMLElement) {
|
|
|
|
|
value.classList.remove('active');
|
|
|
|
|
value.setAttribute('aria-selected', 'false');
|
|
|
|
|
value.setAttribute('tabindex', '-1');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
panels.forEach((panel) => {
|
|
|
|
|
if (panel instanceof HTMLElement) {
|
|
|
|
|
panel.classList.remove('active');
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-03-24 22:00:14 +01:00
|
|
|
tab.classList.add('active');
|
|
|
|
|
tab.setAttribute('aria-selected', 'true');
|
|
|
|
|
tab.setAttribute('tabindex', '0');
|
2026-04-20 23:01:54 +02:00
|
|
|
const panel = root.getElementById(target || '');
|
|
|
|
|
if (panel instanceof HTMLElement) {
|
|
|
|
|
panel.classList.add('active');
|
|
|
|
|
}
|
2026-03-24 22:00:14 +01:00
|
|
|
};
|
|
|
|
|
|
2026-04-20 23:01:54 +02:00
|
|
|
tabs.forEach((tab) => {
|
|
|
|
|
if (!(tab instanceof HTMLElement)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bind(tab, 'click', () => {
|
2026-03-24 22:00:14 +01:00
|
|
|
activateTab(tab);
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-20 23:01:54 +02:00
|
|
|
bind(tab, 'keydown', (event) => {
|
|
|
|
|
if (!(event instanceof KeyboardEvent) || tabs.length === 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const currentIndex = tabs.indexOf(tab);
|
|
|
|
|
if (currentIndex < 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-03-24 22:00:14 +01:00
|
|
|
|
|
|
|
|
let nextIndex = currentIndex;
|
|
|
|
|
if (event.key === 'ArrowRight') {
|
|
|
|
|
nextIndex = (currentIndex + 1) % tabs.length;
|
|
|
|
|
} else if (event.key === 'ArrowLeft') {
|
|
|
|
|
nextIndex = (currentIndex - 1 + tabs.length) % tabs.length;
|
|
|
|
|
} else if (event.key === 'Home') {
|
|
|
|
|
nextIndex = 0;
|
|
|
|
|
} else if (event.key === 'End') {
|
|
|
|
|
nextIndex = tabs.length - 1;
|
|
|
|
|
} else {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
const nextTab = tabs[nextIndex];
|
2026-04-20 23:01:54 +02:00
|
|
|
if (!(nextTab instanceof HTMLElement)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-03-24 22:00:14 +01:00
|
|
|
activateTab(nextTab);
|
|
|
|
|
nextTab.focus();
|
2026-03-24 20:49:22 +01:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-20 23:01:54 +02:00
|
|
|
root.querySelectorAll('.app-error-debug-frame__toggle').forEach((toggle) => {
|
|
|
|
|
bind(toggle, 'click', () => {
|
2026-03-24 22:00:14 +01:00
|
|
|
const frame = toggle.closest('.app-error-debug-frame');
|
2026-04-20 23:01:54 +02:00
|
|
|
if (!(frame instanceof HTMLElement) || !(toggle instanceof HTMLElement)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-03-24 22:00:14 +01:00
|
|
|
const isOpen = frame.classList.toggle('open');
|
|
|
|
|
toggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
|
2026-03-24 20:49:22 +01:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-20 23:01:54 +02:00
|
|
|
root.querySelectorAll('.app-error-debug-frame').forEach((frame) => {
|
2026-03-24 22:00:14 +01:00
|
|
|
const toggle = frame.querySelector('.app-error-debug-frame__toggle');
|
2026-04-20 23:01:54 +02:00
|
|
|
if (!(frame instanceof HTMLElement) || !(toggle instanceof HTMLElement)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-03-24 22:00:14 +01:00
|
|
|
toggle.setAttribute('aria-expanded', frame.classList.contains('open') ? 'true' : 'false');
|
|
|
|
|
});
|
2026-03-24 20:49:22 +01:00
|
|
|
|
2026-04-20 23:01:54 +02:00
|
|
|
root.querySelectorAll('.app-error-debug-query__toggle').forEach((toggle) => {
|
|
|
|
|
bind(toggle, 'click', () => {
|
2026-03-24 22:00:14 +01:00
|
|
|
const query = toggle.closest('.app-error-debug-query');
|
2026-04-20 23:01:54 +02:00
|
|
|
if (!(query instanceof HTMLElement) || !(toggle instanceof HTMLElement)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-03-24 22:00:14 +01:00
|
|
|
const isOpen = query.classList.toggle('open');
|
|
|
|
|
toggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
|
2026-03-24 20:49:22 +01:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-20 23:01:54 +02:00
|
|
|
root.querySelectorAll('.app-error-debug-copy-button').forEach((button) => {
|
|
|
|
|
bind(button, 'click', async (event) => {
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
if (!(button instanceof HTMLElement)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-03-24 20:49:22 +01:00
|
|
|
const text = button.dataset.copy;
|
2026-04-20 23:01:54 +02:00
|
|
|
if (!text || !navigator?.clipboard?.writeText) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-03-24 20:49:22 +01:00
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await navigator.clipboard.writeText(text);
|
|
|
|
|
button.classList.add('copied');
|
|
|
|
|
const original = button.innerHTML;
|
|
|
|
|
button.innerHTML = '<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/></svg>';
|
2026-04-20 23:01:54 +02:00
|
|
|
const timerId = window.setTimeout(() => {
|
2026-03-24 20:49:22 +01:00
|
|
|
button.classList.remove('copied');
|
|
|
|
|
button.innerHTML = original;
|
|
|
|
|
}, 1500);
|
2026-04-20 23:01:54 +02:00
|
|
|
cleanupFns.push(() => window.clearTimeout(timerId));
|
2026-03-24 20:49:22 +01:00
|
|
|
} catch {
|
|
|
|
|
// Clipboard API may not be available in all contexts.
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-04-20 23:01:54 +02:00
|
|
|
|
|
|
|
|
const destroy = () => {
|
|
|
|
|
cleanupFns.forEach((cleanup) => cleanup());
|
|
|
|
|
cleanupFns.length = 0;
|
|
|
|
|
delete body.dataset.errorDebugBound;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return { destroy };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
|
|
|
|
|
const api = initErrorDebug(document);
|
|
|
|
|
window.__appErrorDebugApi = api;
|
|
|
|
|
}
|