feat: create portal component added to view child in new window
This commit is contained in:
@@ -710,6 +710,27 @@ export const ExpandIcon = ({ isActive }: { isActive: boolean }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const ExpandIcon2 = () => {
|
||||
return (
|
||||
<svg
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M9.78365 2H6.6001C6.46676 2 6.33342 2.08334 6.33342 2.21668V2.71672C6.33342 2.85006 6.45009 3.00007 6.6001 3.00007H7.91686C8.06687 3.00007 8.15021 3.16675 8.03353 3.26675L5.20001 6.10028C5.1 6.20028 5.1 6.3503 5.20001 6.4503L5.55003 6.80033C5.65004 6.90033 5.80005 6.90033 5.90005 6.80033L8.73358 3.9668C8.83359 3.86679 9.00026 3.93346 9.00026 4.08347V5.40023C9.00026 5.53357 9.1336 5.68358 9.26695 5.68358H9.75031C9.88366 5.68358 10.0003 5.53357 10.0003 5.40023V2.23335C10.0003 2.08334 9.91699 2 9.78365 2Z"
|
||||
fill="var(--text-color)"
|
||||
/>
|
||||
<path
|
||||
d="M7.71705 5.91777L7.15035 6.50115C7.05034 6.60115 7.00034 6.71783 7.00034 6.85117V8.7513C7.00034 8.88464 6.88366 9.00132 6.75032 9.00132H3.25008C3.11674 9.00132 3.00007 8.88464 3.00007 8.7513V5.25106C3.00007 5.11772 3.11674 5.00104 3.25008 5.00104H5.16688C5.30022 5.00104 5.43357 4.95104 5.5169 4.85103L6.08361 4.28433C6.18362 4.18432 6.11695 4.00098 5.96694 4.00098H2.66671C2.30002 4.00098 2 4.301 2 4.66769V9.33467C2 9.70136 2.30002 10.0014 2.66671 10.0014H7.33369C7.70039 10.0014 8.00041 9.70136 8.00041 9.33467V6.03445C8.00041 5.88444 7.81706 5.81777 7.71705 5.91777Z"
|
||||
fill="var(--text-color)"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const StartIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
|
||||
182
app/src/components/templates/CreateNewWindow.tsx
Normal file
182
app/src/components/templates/CreateNewWindow.tsx
Normal file
@@ -0,0 +1,182 @@
|
||||
import React, { ReactNode, useEffect, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
|
||||
type NewWindowProps = {
|
||||
children: ReactNode;
|
||||
title?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
left?: number;
|
||||
top?: number;
|
||||
center?: boolean;
|
||||
features?: Partial<{
|
||||
toolbar: boolean;
|
||||
menubar: boolean;
|
||||
scrollbars: boolean;
|
||||
resizable: boolean;
|
||||
location: boolean;
|
||||
status: boolean;
|
||||
}>;
|
||||
onClose?: () => void;
|
||||
copyStyles?: boolean;
|
||||
noopener?: boolean;
|
||||
className?: string;
|
||||
theme?: string | null;
|
||||
};
|
||||
|
||||
export const RenderInNewWindow: React.FC<NewWindowProps> = ({
|
||||
children,
|
||||
title = "New Window",
|
||||
width = 900,
|
||||
height = 700,
|
||||
left,
|
||||
top,
|
||||
center = true,
|
||||
features,
|
||||
onClose,
|
||||
copyStyles = true,
|
||||
noopener = true,
|
||||
className,
|
||||
theme = "light",
|
||||
}) => {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const childWindowRef = useRef<Window | null>(null);
|
||||
const containerElRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
const screenLeft = window.screenLeft ?? window.screenX ?? 0;
|
||||
const screenTop = window.screenTop ?? window.screenY ?? 0;
|
||||
const availWidth = window.outerWidth ?? window.innerWidth;
|
||||
const availHeight = window.outerHeight ?? window.innerHeight;
|
||||
|
||||
const finalLeft =
|
||||
center && availWidth
|
||||
? Math.max(0, screenLeft + (availWidth - width) / 2)
|
||||
: left ?? 100;
|
||||
|
||||
const finalTop =
|
||||
center && availHeight
|
||||
? Math.max(0, screenTop + (availHeight - height) / 2)
|
||||
: top ?? 100;
|
||||
|
||||
const baseFeatures = [
|
||||
`width=${Math.floor(width)}`,
|
||||
`height=${Math.floor(height)}`,
|
||||
`left=${Math.floor(finalLeft)}`,
|
||||
`top=${Math.floor(finalTop)}`,
|
||||
];
|
||||
|
||||
const featureFlags = features ?? {
|
||||
toolbar: false,
|
||||
menubar: false,
|
||||
scrollbars: true,
|
||||
resizable: true,
|
||||
location: false,
|
||||
status: false,
|
||||
};
|
||||
|
||||
Object.entries(featureFlags).forEach(([k, v]) =>
|
||||
baseFeatures.push(`${k}=${v ? "yes" : "no"}`)
|
||||
);
|
||||
|
||||
const newWin = window.open("", "_blank", baseFeatures.join(","));
|
||||
if (!newWin) {
|
||||
console.warn("Popup blocked or failed to open window.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (noopener) {
|
||||
try {
|
||||
newWin.opener = null;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
newWin.document.open();
|
||||
newWin.document.write(`<!doctype html>
|
||||
<html data-theme=${theme}>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>${title}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
</head>
|
||||
<body style="margin:0;"></body>
|
||||
</html>`);
|
||||
newWin.document.close();
|
||||
|
||||
if (copyStyles) {
|
||||
const head = newWin.document.head;
|
||||
Array.from(document.styleSheets).forEach((styleSheet) => {
|
||||
try {
|
||||
if ((styleSheet as CSSStyleSheet).cssRules) {
|
||||
const newStyleEl = newWin.document.createElement("style");
|
||||
const rules = Array.from(
|
||||
(styleSheet as CSSStyleSheet).cssRules
|
||||
).map((r) => r.cssText);
|
||||
newStyleEl.appendChild(
|
||||
newWin.document.createTextNode(rules.join("\n"))
|
||||
);
|
||||
head.appendChild(newStyleEl);
|
||||
}
|
||||
} catch {
|
||||
const ownerNode = styleSheet.ownerNode as HTMLElement | null;
|
||||
if (ownerNode && ownerNode.tagName === "LINK") {
|
||||
const link = ownerNode as HTMLLinkElement;
|
||||
const newLink = newWin.document.createElement("link");
|
||||
newLink.rel = link.rel;
|
||||
newLink.href = link.href;
|
||||
newLink.media = link.media;
|
||||
head.appendChild(newLink);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const container = newWin.document.createElement("div");
|
||||
if (className) container.className = className;
|
||||
newWin.document.body.appendChild(container);
|
||||
|
||||
newWin.document.title = title;
|
||||
|
||||
// Handle child window close
|
||||
const handleChildUnload = () => {
|
||||
onClose?.();
|
||||
};
|
||||
newWin.addEventListener("beforeunload", handleChildUnload);
|
||||
|
||||
// 👇 Handle parent refresh/close → auto close child
|
||||
const handleParentUnload = () => {
|
||||
try {
|
||||
newWin.close();
|
||||
} catch {}
|
||||
};
|
||||
window.addEventListener("beforeunload", handleParentUnload);
|
||||
|
||||
childWindowRef.current = newWin;
|
||||
containerElRef.current = container;
|
||||
setMounted(true);
|
||||
|
||||
return () => {
|
||||
newWin.removeEventListener("beforeunload", handleChildUnload);
|
||||
window.removeEventListener("beforeunload", handleParentUnload);
|
||||
try {
|
||||
newWin.close();
|
||||
} catch {}
|
||||
childWindowRef.current = null;
|
||||
containerElRef.current = null;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const w = childWindowRef.current;
|
||||
if (w && !w.closed) {
|
||||
w.document.title = title;
|
||||
}
|
||||
}, [title]);
|
||||
|
||||
if (!mounted || !containerElRef.current) return null;
|
||||
|
||||
return createPortal(children, containerElRef.current);
|
||||
};
|
||||
@@ -1,70 +1,31 @@
|
||||
// LogList.tsx
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { LogListIcon, CloseIcon } from "../../icons/ExportCommonIcons"; // Adjust path as needed
|
||||
import { useLogger } from "./LoggerContext";
|
||||
import {
|
||||
LogListIcon,
|
||||
CloseIcon,
|
||||
ExpandIcon2,
|
||||
} from "../../icons/ExportCommonIcons"; // Adjust path as needed
|
||||
import { LogEntry, useLogger } from "./LoggerContext";
|
||||
import { GetLogIcon } from "../../footer/getLogIcons";
|
||||
import { RenderInNewWindow } from "../../templates/CreateNewWindow";
|
||||
|
||||
const LogList: React.FC = () => {
|
||||
const {
|
||||
logs,
|
||||
clear,
|
||||
setIsLogListVisible,
|
||||
isLogListVisible,
|
||||
// --- Logs Component ---
|
||||
type LogsProps = {
|
||||
selectedTab: "all" | "info" | "warning" | "error";
|
||||
setSelectedTab: (tab: "all" | "info" | "warning" | "error") => void;
|
||||
clear: () => void;
|
||||
filteredLogs: LogEntry[];
|
||||
formatTimestamp: (date: Date) => string;
|
||||
};
|
||||
|
||||
const Logs: React.FC<LogsProps> = ({
|
||||
selectedTab,
|
||||
setSelectedTab,
|
||||
} = useLogger();
|
||||
|
||||
const formatTimestamp = (date: Date) => new Date(date).toLocaleTimeString();
|
||||
|
||||
const filteredLogs =
|
||||
selectedTab === "all"
|
||||
? [...logs].reverse()
|
||||
: [...logs].filter((log) => log.type === selectedTab).reverse();
|
||||
|
||||
useEffect(() => {
|
||||
if (isLogListVisible && logs.length > 0) {
|
||||
const lastLog = logs[logs.length - 1];
|
||||
const validTypes = ["all", "info", "warning", "error"];
|
||||
|
||||
if (validTypes.includes(lastLog.type)) {
|
||||
setSelectedTab(lastLog.type);
|
||||
} else {
|
||||
setSelectedTab("all");
|
||||
}
|
||||
}
|
||||
}, [isLogListVisible]);
|
||||
|
||||
clear,
|
||||
filteredLogs,
|
||||
formatTimestamp,
|
||||
}) => {
|
||||
return (
|
||||
// eslint-disable-next-line
|
||||
<div
|
||||
className="log-list-container"
|
||||
onClick={() => setIsLogListVisible(false)}
|
||||
>
|
||||
{/* eslint-disable-next-line */}
|
||||
<div
|
||||
className="log-list-wrapper"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<div className="log-header">
|
||||
<div className="log-header-wrapper">
|
||||
<div className="icon">
|
||||
<LogListIcon />
|
||||
</div>
|
||||
<div className="head">Log List</div>
|
||||
</div>
|
||||
<button
|
||||
id="close-btn"
|
||||
title="close-btn"
|
||||
className="close"
|
||||
onClick={() => setIsLogListVisible(false)}
|
||||
>
|
||||
<CloseIcon />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<>
|
||||
<div className="log-nav-container">
|
||||
<div className="log-nav-wrapper">
|
||||
{["all", "info", "warning", "error"].map((type) => (
|
||||
@@ -109,8 +70,113 @@ const LogList: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// --- LogList Component ---
|
||||
const LogList: React.FC = () => {
|
||||
const {
|
||||
logs,
|
||||
clear,
|
||||
setIsLogListVisible,
|
||||
isLogListVisible,
|
||||
selectedTab,
|
||||
setSelectedTab,
|
||||
} = useLogger();
|
||||
|
||||
const formatTimestamp = (date: Date) => new Date(date).toLocaleTimeString();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const filteredLogs =
|
||||
selectedTab === "all"
|
||||
? [...logs].reverse()
|
||||
: [...logs].filter((log) => log.type === selectedTab).reverse();
|
||||
|
||||
useEffect(() => {
|
||||
if (isLogListVisible && logs.length > 0) {
|
||||
const lastLog = logs[logs.length - 1];
|
||||
const validTypes = ["all", "info", "warning", "error"];
|
||||
if (validTypes.includes(lastLog.type)) {
|
||||
setSelectedTab(lastLog.type as any);
|
||||
} else {
|
||||
setSelectedTab("all");
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
}, [isLogListVisible]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!open ? (
|
||||
<div
|
||||
className="log-list-container"
|
||||
onClick={() => setIsLogListVisible(false)}
|
||||
>
|
||||
{/* eslint-disable-next-line */}
|
||||
<div
|
||||
className="log-list-wrapper"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<div className="log-header">
|
||||
<div className="log-header-wrapper">
|
||||
<div className="icon">
|
||||
<LogListIcon />
|
||||
</div>
|
||||
<div className="head">Log List</div>
|
||||
</div>
|
||||
<div className="action-buttons-container">
|
||||
<button
|
||||
id="expand-log-btn"
|
||||
title="open in new tab"
|
||||
className="expand-btn"
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
<ExpandIcon2 />
|
||||
</button>
|
||||
<button
|
||||
id="close-btn"
|
||||
title="close"
|
||||
className="close"
|
||||
onClick={() => setIsLogListVisible(false)}
|
||||
>
|
||||
<CloseIcon />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Logs Section */}
|
||||
<Logs
|
||||
selectedTab={selectedTab as any}
|
||||
setSelectedTab={setSelectedTab as any}
|
||||
clear={clear}
|
||||
filteredLogs={filteredLogs}
|
||||
formatTimestamp={formatTimestamp}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<RenderInNewWindow
|
||||
title="Log list"
|
||||
theme={localStorage.getItem("theme")}
|
||||
onClose={() => setOpen(false)}
|
||||
>
|
||||
<div className="log-list-new-window-wrapper">
|
||||
<Logs
|
||||
selectedTab={selectedTab as any}
|
||||
setSelectedTab={setSelectedTab as any}
|
||||
clear={clear}
|
||||
filteredLogs={filteredLogs}
|
||||
formatTimestamp={formatTimestamp}
|
||||
/>
|
||||
</div>
|
||||
</RenderInNewWindow>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,53 +1,6 @@
|
||||
@use "../../abstracts/variables.scss" as *;
|
||||
@use "../../abstracts/mixins.scss" as *;
|
||||
|
||||
.log-list-container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: var(--background-color-secondary);
|
||||
@include flex-center;
|
||||
|
||||
.log-list-wrapper {
|
||||
height: 60%;
|
||||
min-width: 55%;
|
||||
z-index: 5;
|
||||
background: var(--background-color);
|
||||
padding: 14px 12px;
|
||||
border-radius: #{$border-radius-xlarge};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
backdrop-filter: blur(50px);
|
||||
outline: 1px solid var(--border-color);
|
||||
|
||||
.log-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.log-header-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
.icon {
|
||||
@include flex-center;
|
||||
scale: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.close {
|
||||
@include flex-center;
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
cursor: pointer;
|
||||
border-radius: #{$border-radius-medium};
|
||||
svg {
|
||||
scale: 1.6;
|
||||
}
|
||||
&:hover {
|
||||
background: var(--background-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
.log-nav-container {
|
||||
@include flex-space-between;
|
||||
align-items: flex-end;
|
||||
@@ -121,11 +74,74 @@
|
||||
background: var(--background-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
.no-log {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: var(--text-color);
|
||||
}
|
||||
}
|
||||
|
||||
.log-list-container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: var(--background-color-secondary);
|
||||
@include flex-center;
|
||||
|
||||
.log-list-wrapper {
|
||||
height: 60%;
|
||||
min-width: 55%;
|
||||
z-index: 5;
|
||||
background: var(--background-color);
|
||||
padding: 14px 12px;
|
||||
border-radius: #{$border-radius-xlarge};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
backdrop-filter: blur(50px);
|
||||
outline: 1px solid var(--border-color);
|
||||
|
||||
.log-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.log-header-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
.icon {
|
||||
@include flex-center;
|
||||
scale: 0.8;
|
||||
}
|
||||
}
|
||||
.action-buttons-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
.close,
|
||||
.expand-btn {
|
||||
@include flex-center;
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
cursor: pointer;
|
||||
border-radius: #{$border-radius-medium};
|
||||
svg {
|
||||
scale: 1.6;
|
||||
}
|
||||
&:hover {
|
||||
background: var(--background-color);
|
||||
border: 1px solid #ffffff29;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.log-list-new-window-wrapper{
|
||||
padding: 10px;
|
||||
background: var(--background-color-solid);
|
||||
height: 100vh;
|
||||
.log-nav-container{
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user