1
0
mirror of https://github.com/alexandrev/xslt-lab.git synced 2026-09-18 19:43:16 +00:00

fix pagedev report

This commit is contained in:
2026-01-18 15:45:42 +01:00
parent 9d50dbf956
commit 65dcfc7b10
6 changed files with 273 additions and 100 deletions
+27 -11
View File
@@ -23,18 +23,34 @@
<link rel="canonical" href="https://xsltplayground.com/" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<script type="module" src="/env.js"></script>
<script>
if (window.env && window.env.VITE_ADSENSE_CLIENT) {
const s = document.createElement('script');
s.async = true;
s.src =
'https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=' +
window.env.VITE_ADSENSE_CLIENT;
s.crossOrigin = 'anonymous';
document.head.appendChild(s);
<script>
const loadAdsense = () => {
const client = window.env && window.env.VITE_ADSENSE_CLIENT;
if (!client) return;
const s = document.createElement("script");
s.async = true;
s.src =
"https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=" +
client;
s.crossOrigin = "anonymous";
document.head.appendChild(s);
};
const scheduleAdsense = () => {
if ("requestIdleCallback" in window) {
window.requestIdleCallback(loadAdsense, { timeout: 2500 });
} else {
window.setTimeout(loadAdsense, 1500);
}
</script>
</head>
};
if (document.readyState === "complete") {
scheduleAdsense();
} else {
window.addEventListener("load", scheduleAdsense, { once: true });
}
</script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
+154 -63
View File
@@ -1,19 +1,7 @@
import { useState, useEffect, useCallback, useRef, useMemo } from "react";
import GA4React from "ga-4-react";
import Editor from "@monaco-editor/react";
import formatXML from "xml-formatter";
import { initializeApp } from "firebase/app";
import {
getAuth,
GoogleAuthProvider,
signInWithPopup,
signOut,
} from "firebase/auth";
import { useState, useEffect, useCallback, useRef, useMemo, lazy, Suspense } from "react";
import logo from "./logo.svg";
import TabsNav from "./components/TabsNav";
import DataPipelineHeader from "./components/DataPipelineHeader";
import FeedbackWidget from "./components/FeedbackWidget";
import BuyMeACoffee from "./components/BuyMeACoffee";
import Icon from "./components/Icon";
import {
parseErrorLines,
@@ -26,6 +14,42 @@ import {
/* global __APP_VERSION__ */
const MonacoEditor = lazy(() => import("@monaco-editor/react"));
const FeedbackWidget = lazy(() => import("./components/FeedbackWidget"));
const BuyMeACoffee = lazy(() => import("./components/BuyMeACoffee"));
function runWhenIdle(callback, timeout = 2000) {
if (typeof window === "undefined") {
return () => {};
}
if ("requestIdleCallback" in window) {
const id = window.requestIdleCallback(callback, { timeout });
return () => window.cancelIdleCallback?.(id);
}
const id = window.setTimeout(callback, timeout);
return () => window.clearTimeout(id);
}
function Editor({ height, ...props }) {
const fallbackStyle = height ? { height } : undefined;
return (
<Suspense
fallback={
<div
className="editor-loading"
role="status"
aria-live="polite"
style={fallbackStyle}
>
Loading editor...
</div>
}
>
<MonacoEditor height={height} {...props} />
</Suspense>
);
}
function debounce(fn, delay) {
let t;
return (...args) => {
@@ -273,6 +297,7 @@ export default function App() {
const [viewportWidth, setViewportWidth] = useState(() =>
typeof window !== "undefined" ? window.innerWidth : 0,
);
const [widgetsReady, setWidgetsReady] = useState(false);
const ethicalSlotRef = useRef(null);
const isLocalhost =
typeof window !== "undefined" &&
@@ -291,12 +316,32 @@ export default function App() {
const ethicalAdVariant = `${isCompactEthicalAd ? "compact" : "wide"}-${ethicalAdType}`;
const backendBase = (env.VITE_BACKEND_URL || "").replace(/\/$/, "");
console.log("Using this URL as backendURL:", backendBase);
useEffect(() => {
const ga4react = new GA4React(env.VITE_GA_ID);
ga4react.initialize().catch(err => console.error(err));
}, []);
const gaId = env.VITE_GA_ID;
if (!gaId) return;
let cancelled = false;
const cancelIdle = runWhenIdle(async () => {
if (cancelled) return;
try {
const { default: GA4React } = await import("ga-4-react");
if (cancelled) return;
const ga4react = new GA4React(gaId);
ga4react.initialize().catch((err) => console.error(err));
} catch (err) {
console.error(err);
}
}, 2500);
return () => {
cancelled = true;
cancelIdle?.();
};
}, []);
useEffect(() => {
const cancelIdle = runWhenIdle(() => setWidgetsReady(true), 2000);
return () => cancelIdle?.();
}, []);
// Persist workspace on change
useEffect(() => {
@@ -597,16 +642,28 @@ export default function App() {
useEffect(() => {
if (!goPro) return;
try {
const cfg = env.VITE_FIREBASE_CONFIG;
if (cfg) {
let cancelled = false;
const cancelIdle = runWhenIdle(async () => {
if (cancelled) return;
try {
const cfg = env.VITE_FIREBASE_CONFIG;
if (!cfg) return;
const [{ initializeApp }, { getAuth }] = await Promise.all([
import("firebase/app"),
import("firebase/auth"),
]);
if (cancelled) return;
const app = initializeApp(JSON.parse(cfg));
const a = getAuth(app);
setAuth(a);
a.onAuthStateChanged((u) => setUser(u));
}
} catch {}
}, []);
} catch {}
}, 2500);
return () => {
cancelled = true;
cancelIdle?.();
};
}, [goPro]);
useEffect(() => {
tabsRef.current = tabs;
@@ -1174,11 +1231,14 @@ export default function App() {
);
useEffect(() => {
if (adsenseClient && adsenseSlot && window.adsbygoogle) {
if (!adsenseClient || !adsenseSlot) return;
const cancelIdle = runWhenIdle(() => {
if (!window.adsbygoogle) return;
try {
window.adsbygoogle.push({});
} catch {}
}
}, 2000);
return () => cancelIdle?.();
}, []);
useEffect(() => {
@@ -1190,25 +1250,36 @@ export default function App() {
setEthicalAdsReady(true);
return;
}
const existing = document.querySelector("script[data-ethicalads]");
const script = existing || document.createElement("script");
if (!existing) {
script.src = "https://media.ethicalads.io/media/client/ethicalads.min.js";
script.async = true;
script.dataset.ethicalads = "true";
document.body.appendChild(script);
}
const onLoad = () => setEthicalAdsReady(Boolean(window.ethicalads));
const onError = () => setEthicalAdsReady(false);
script.addEventListener("load", onLoad);
script.addEventListener("error", onError);
const fallback = window.setTimeout(() => {
if (!window.ethicalads) setEthicalAdsReady(false);
}, 3500);
let cancelled = false;
let script = null;
let fallback = null;
let onLoad = null;
let onError = null;
const loadEthicalAds = () => {
if (cancelled) return;
const existing = document.querySelector("script[data-ethicalads]");
script = existing || document.createElement("script");
if (!existing) {
script.src = "https://media.ethicalads.io/media/client/ethicalads.min.js";
script.async = true;
script.dataset.ethicalads = "true";
document.body.appendChild(script);
}
onLoad = () => setEthicalAdsReady(Boolean(window.ethicalads));
onError = () => setEthicalAdsReady(false);
script.addEventListener("load", onLoad);
script.addEventListener("error", onError);
fallback = window.setTimeout(() => {
if (!window.ethicalads) setEthicalAdsReady(false);
}, 3500);
};
const cancelIdle = runWhenIdle(loadEthicalAds, 2000);
return () => {
script.removeEventListener("load", onLoad);
script.removeEventListener("error", onError);
window.clearTimeout(fallback);
cancelled = true;
cancelIdle?.();
if (script && onLoad) script.removeEventListener("load", onLoad);
if (script && onError) script.removeEventListener("error", onError);
if (fallback) window.clearTimeout(fallback);
};
}, [ethicalAdsEnabled]);
@@ -1220,17 +1291,6 @@ export default function App() {
} catch {}
}, [ethicalAdsEnabled, ethicalAdsReady, ethicalAdVariant]);
useEffect(() => {
if (!ethicalAdsPublisher) return;
const existing = document.querySelector("script[data-ethicalads]");
if (existing) return;
const script = document.createElement("script");
script.src = "https://media.ethicalads.io/media/client/ethicalads.min.js";
script.async = true;
script.dataset.ethicalads = "true";
document.body.appendChild(script);
}, []);
return (
<div className="app-container">
{ethicalAdsEnabled && (
@@ -1284,6 +1344,7 @@ export default function App() {
accept="application/json,.json"
className="file-input"
onChange={handleWorkspaceImport}
aria-label="Import workspace file"
/>
</div>
</div>
@@ -1336,6 +1397,7 @@ export default function App() {
placeholder="Parameter name"
value={p.name}
onChange={(e) => updateParam(i, "name", e.target.value)}
aria-label="Parameter name"
/>
</div>
<button
@@ -1371,13 +1433,18 @@ export default function App() {
/>
</div>
<div className="param-footer">
<label className="icon-button file-label param-upload">
<label
className="icon-button file-label param-upload"
aria-label="Upload parameter value"
title="Upload parameter value"
>
<Icon name="upload" />
<input
type="file"
accept=".xml"
className="file-input"
onChange={(e) => loadFile(e, (t) => updateParam(i, "value", t))}
aria-label="Upload parameter value file"
/>
</label>
<button
@@ -1425,6 +1492,7 @@ export default function App() {
),
)
}
aria-label="XSLT version"
>
<option value="1.0">XSLT 1.0</option>
<option value="2.0">XSLT 2.0</option>
@@ -1439,12 +1507,17 @@ export default function App() {
<span className="trace-toggle-label">Enable Internal Variables</span>
</label>
<div className="right-actions">
<label className="icon-button file-label">
<label
className="icon-button file-label"
aria-label="Upload stylesheet"
title="Upload stylesheet"
>
<Icon name="upload" />
<input
type="file"
accept=".xsl,.xslt"
className="file-input"
aria-label="Upload stylesheet file"
onChange={(e) =>
loadFile(e, (t) =>
setTabs((tabs) =>
@@ -1460,6 +1533,8 @@ export default function App() {
</label>
<button
className="icon-button"
aria-label="Download stylesheet"
title="Download stylesheet"
onClick={() =>
download(
injectParamBlock(activeTab.xslt, activeTab.params),
@@ -1527,6 +1602,7 @@ export default function App() {
onClick={handleCopyAllTrace}
type="button"
disabled={!traceEntries.length && !traceText}
aria-label="Copy all trace variables"
>
<Icon name="copy" />
</button>
@@ -1543,6 +1619,7 @@ export default function App() {
}
}}
type="button"
aria-label={showRawTrace ? "Hide raw trace output" : "Show raw trace output"}
>
<Icon name={showRawTrace ? "terminal-off" : "terminal"} />
</button>
@@ -1639,7 +1716,7 @@ export default function App() {
}}
>
{error && !errorCollapsed && (
<div className="error-box">
<div className="error-box" role="alert" aria-live="assertive">
<div className="error-box-header">
<span>Errors</span>
<div className="error-box-actions">
@@ -1710,7 +1787,9 @@ export default function App() {
{showResultPane && (
<>
{duration !== null && (
<div className="success-box">Success in {duration} ms</div>
<div className="success-box" role="status" aria-live="polite">
Success in {duration} ms
</div>
)}
<div className="result-actions">
{canRenderHtml && (
@@ -1742,9 +1821,10 @@ export default function App() {
<button
className="icon-button result-format-button"
disabled={effectiveResultView !== "source"}
onClick={() => {
onClick={async () => {
if (effectiveResultView !== "source") return;
try {
const { default: formatXML } = await import("xml-formatter");
const formatted = formatXML(result);
if (activeTab) {
updateWorkspaceStatus(activeTab.id, (prev) => ({
@@ -1776,6 +1856,7 @@ export default function App() {
title="Rendered HTML output"
srcDoc={result || "<!-- empty -->"}
sandbox=""
loading="lazy"
/>
</div>
) : (
@@ -1800,7 +1881,7 @@ export default function App() {
</div>
<div className="footer">
<div className="footer-left">
<img src={logo} alt="logo" className="logo" />
<img src={logo} alt="XSLT Playground logo" className="logo" />
<strong>xsltplayground.com</strong>
<a
className="news-link"
@@ -1853,15 +1934,25 @@ export default function App() {
}}
>
<div className="trace-hover-actions">
<button type="button" className="icon-button" onClick={copyTraceHover} title="Copy value">
<button
type="button"
className="icon-button"
onClick={copyTraceHover}
title="Copy value"
aria-label="Copy value"
>
<Icon name="copy" />
</button>
</div>
<pre>{traceHover.text}</pre>
</div>
)}
<BuyMeACoffee />
<FeedbackWidget />
{widgetsReady && (
<Suspense fallback={null}>
<BuyMeACoffee />
<FeedbackWidget />
</Suspense>
)}
</div>
);
}
+41 -22
View File
@@ -9,32 +9,51 @@ export default function BuyMeACoffee() {
if (!containerRef.current) return;
if (document.getElementById(SCRIPT_ID)) return;
const script = document.createElement("script");
script.id = SCRIPT_ID;
script.setAttribute("data-name", "BMC-Widget");
script.src = "https://cdnjs.buymeacoffee.com/1.0.0/widget.prod.min.js";
script.setAttribute("data-id", "alexandrev");
script.setAttribute("data-description", "Support me on Buy me a coffee!");
script.setAttribute(
"data-message",
"Thanks for using XSLT Playground. If it helped you, feel free to buy me a coffee ☕",
);
script.setAttribute("data-color", "#FFDD00");
script.setAttribute("data-position", "Right");
script.setAttribute("data-x_margin", "24");
script.setAttribute("data-y_margin", "24");
script.async = true;
containerRef.current.appendChild(script);
let cancelled = false;
let script = null;
let cancelIdle = null;
let handleLoad = null;
const loadWidget = () => {
if (cancelled || !containerRef.current) return;
script = document.createElement("script");
script.id = SCRIPT_ID;
script.setAttribute("data-name", "BMC-Widget");
script.src = "https://cdnjs.buymeacoffee.com/1.0.0/widget.prod.min.js";
script.setAttribute("data-id", "alexandrev");
script.setAttribute("data-description", "Support me on Buy me a coffee!");
script.setAttribute(
"data-message",
"Thanks for using XSLT Playground. If it helped you, feel free to buy me a coffee ☕",
);
script.setAttribute("data-color", "#FFDD00");
script.setAttribute("data-position", "Right");
script.setAttribute("data-x_margin", "24");
script.setAttribute("data-y_margin", "24");
script.async = true;
containerRef.current.appendChild(script);
const handleLoad = () => {
const evt = document.createEvent("Event");
evt.initEvent("DOMContentLoaded", false, false);
window.dispatchEvent(evt);
handleLoad = () => {
const evt = document.createEvent("Event");
evt.initEvent("DOMContentLoaded", false, false);
window.dispatchEvent(evt);
};
script.addEventListener("load", handleLoad);
};
script.addEventListener("load", handleLoad);
if ("requestIdleCallback" in window) {
const idleId = window.requestIdleCallback(loadWidget, { timeout: 2500 });
cancelIdle = () => window.cancelIdleCallback?.(idleId);
} else {
const timerId = window.setTimeout(loadWidget, 1800);
cancelIdle = () => window.clearTimeout(timerId);
}
return () => {
script.removeEventListener("load", handleLoad);
cancelled = true;
cancelIdle?.();
if (script && handleLoad) {
script.removeEventListener("load", handleLoad);
}
};
}, []);
@@ -225,8 +225,6 @@ export default function FeedbackWidget() {
<div
className="feedback-header"
onMouseDown={startDrag}
role="button"
tabIndex={0}
>
<span>Feedback</span>
<button
+1
View File
@@ -83,6 +83,7 @@ export default function TabsNav({
placeholder={`Workspace ${index + 1}`}
onChange={(e) => setDraftName(e.target.value)}
onBlur={() => commitEditing(tab)}
aria-label="Workspace name"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
+50 -2
View File
@@ -7,6 +7,12 @@ body,
background: #f5f5f5;
}
a:focus-visible {
outline: none;
box-shadow: 0 0 0 2px rgba(74, 133, 255, 0.35);
border-radius: 4px;
}
.news-link {
color: #007acc;
text-decoration: none;
@@ -88,6 +94,12 @@ body,
text-overflow: ellipsis;
}
.tab-button:focus-visible {
outline: none;
box-shadow: 0 0 0 2px rgba(74, 133, 255, 0.35);
border-radius: 6px;
}
.tab-name-input {
max-width: 160px;
padding: 0.35rem 0.6rem;
@@ -114,7 +126,7 @@ body,
width: 28px;
height: 28px;
font-size: 0.85rem;
color: #777;
color: #4b5563;
}
.tab-close:hover {
@@ -594,6 +606,18 @@ body,
min-width: 0; /* allow editor to shrink */
}
.editor-loading {
height: 100%;
min-height: 120px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: #4b5563;
background: #f0f4ff;
font-size: 0.9rem;
}
.trace-panel {
width: 30%;
background: #fff;
@@ -919,7 +943,15 @@ body,
}
.file-input {
display: none;
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
white-space: nowrap;
}
.file-label {
@@ -927,6 +959,12 @@ body,
align-items: center;
}
.file-label:focus-within {
outline: none;
box-shadow: 0 0 0 2px rgba(74, 133, 255, 0.25);
border-radius: 8px;
}
.result-actions {
position: absolute;
top: 0.35rem;
@@ -1210,6 +1248,12 @@ body,
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.5);
}
:root[data-theme="dark"] .editor-loading {
background: #111923;
color: #c7d2e3;
border: 1px solid #2b3645;
}
:root[data-theme="dark"] .param-footer .icon-button {
border-color: #2b3645;
background: #1a2330;
@@ -1336,6 +1380,10 @@ body,
box-shadow: 0 0 0 2px rgba(107, 182, 255, 0.25);
}
:root[data-theme="dark"] .file-label:focus-within {
box-shadow: 0 0 0 2px rgba(107, 182, 255, 0.35);
}
:root[data-theme="dark"] .footer-theme-toggle.active {
color: #6bb6ff;
}