Spaces:
Sleeping
Sleeping
keep custom iframe with a throttle
Browse files- components/editor/index.tsx +2 -4
- components/editor/preview/index.tsx +128 -130
components/editor/index.tsx
CHANGED
|
@@ -12,7 +12,6 @@ import {
|
|
| 12 |
useUnmount,
|
| 13 |
useUpdateEffect,
|
| 14 |
} from "react-use";
|
| 15 |
-
import { SandpackPreviewRef } from "@codesandbox/sandpack-react";
|
| 16 |
import classNames from "classnames";
|
| 17 |
import { useRouter, useSearchParams } from "next/navigation";
|
| 18 |
|
|
@@ -47,7 +46,7 @@ export const AppEditor = ({
|
|
| 47 |
const router = useRouter();
|
| 48 |
const deploy = searchParams.get("deploy") === "true";
|
| 49 |
|
| 50 |
-
const iframeRef = useRef<
|
| 51 |
const preview = useRef<HTMLDivElement>(null);
|
| 52 |
const editor = useRef<HTMLDivElement>(null);
|
| 53 |
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
|
|
@@ -356,7 +355,6 @@ export const AppEditor = ({
|
|
| 356 |
ref={preview}
|
| 357 |
device={device}
|
| 358 |
pages={pages}
|
| 359 |
-
currentPage={currentPage}
|
| 360 |
setCurrentPage={setCurrentPage}
|
| 361 |
currentTab={currentTab}
|
| 362 |
isEditableModeEnabled={isEditableModeEnabled}
|
|
@@ -386,7 +384,7 @@ export const AppEditor = ({
|
|
| 386 |
}}
|
| 387 |
htmlHistory={htmlHistory}
|
| 388 |
setPages={setPages}
|
| 389 |
-
|
| 390 |
device={device}
|
| 391 |
setDevice={setDevice}
|
| 392 |
/>
|
|
|
|
| 12 |
useUnmount,
|
| 13 |
useUpdateEffect,
|
| 14 |
} from "react-use";
|
|
|
|
| 15 |
import classNames from "classnames";
|
| 16 |
import { useRouter, useSearchParams } from "next/navigation";
|
| 17 |
|
|
|
|
| 46 |
const router = useRouter();
|
| 47 |
const deploy = searchParams.get("deploy") === "true";
|
| 48 |
|
| 49 |
+
const iframeRef = useRef<HTMLIFrameElement | null>(null);
|
| 50 |
const preview = useRef<HTMLDivElement>(null);
|
| 51 |
const editor = useRef<HTMLDivElement>(null);
|
| 52 |
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
|
|
|
|
| 355 |
ref={preview}
|
| 356 |
device={device}
|
| 357 |
pages={pages}
|
|
|
|
| 358 |
setCurrentPage={setCurrentPage}
|
| 359 |
currentTab={currentTab}
|
| 360 |
isEditableModeEnabled={isEditableModeEnabled}
|
|
|
|
| 384 |
}}
|
| 385 |
htmlHistory={htmlHistory}
|
| 386 |
setPages={setPages}
|
| 387 |
+
iframeRef={iframeRef}
|
| 388 |
device={device}
|
| 389 |
setDevice={setDevice}
|
| 390 |
/>
|
components/editor/preview/index.tsx
CHANGED
|
@@ -3,13 +3,7 @@ import { useUpdateEffect } from "react-use";
|
|
| 3 |
import { useMemo, useState } from "react";
|
| 4 |
import classNames from "classnames";
|
| 5 |
import { toast } from "sonner";
|
| 6 |
-
import {
|
| 7 |
-
SandpackLayout,
|
| 8 |
-
SandpackPreview,
|
| 9 |
-
SandpackPreviewRef,
|
| 10 |
-
SandpackProvider,
|
| 11 |
-
useSandpack,
|
| 12 |
-
} from "@codesandbox/sandpack-react";
|
| 13 |
|
| 14 |
import { cn } from "@/lib/utils";
|
| 15 |
import { GridPattern } from "@/components/magic-ui/grid-pattern";
|
|
@@ -17,7 +11,7 @@ import { htmlTagToText } from "@/lib/html-tag-to-text";
|
|
| 17 |
import { Page } from "@/types";
|
| 18 |
|
| 19 |
export const Preview = ({
|
| 20 |
-
|
| 21 |
isResizing,
|
| 22 |
isAiWorking,
|
| 23 |
ref,
|
|
@@ -25,87 +19,126 @@ export const Preview = ({
|
|
| 25 |
currentTab,
|
| 26 |
iframeRef,
|
| 27 |
pages,
|
| 28 |
-
|
| 29 |
-
// setCurrentPage,
|
| 30 |
isEditableModeEnabled,
|
| 31 |
-
|
| 32 |
-
{
|
| 33 |
html: string;
|
| 34 |
isResizing: boolean;
|
| 35 |
isAiWorking: boolean;
|
| 36 |
pages: Page[];
|
| 37 |
-
currentPage: string;
|
| 38 |
setCurrentPage: React.Dispatch<React.SetStateAction<string>>;
|
| 39 |
ref: React.RefObject<HTMLDivElement | null>;
|
| 40 |
-
iframeRef?: React.RefObject<
|
| 41 |
device: "desktop" | "mobile";
|
| 42 |
currentTab: string;
|
| 43 |
isEditableModeEnabled?: boolean;
|
| 44 |
onClickElement?: (element: HTMLElement) => void;
|
| 45 |
}) => {
|
| 46 |
-
const [hoveredElement] = useState<HTMLElement | null>(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
// const targetElement = event.target as HTMLElement;
|
| 54 |
-
// const iframeEl = iframeDocument as HTMLIFrameElement;
|
| 55 |
-
// const iframeBody = iframeEl.contentDocument?.body;
|
| 56 |
-
// if (hoveredElement !== targetElement && targetElement !== iframeBody) {
|
| 57 |
-
// setHoveredElement(targetElement);
|
| 58 |
-
// targetElement.classList.add("hovered-element");
|
| 59 |
-
// } else {
|
| 60 |
-
// return setHoveredElement(null);
|
| 61 |
-
// }
|
| 62 |
-
// }
|
| 63 |
-
// }
|
| 64 |
-
// };
|
| 65 |
-
// const handleMouseOut = () => {
|
| 66 |
-
// setHoveredElement(null);
|
| 67 |
-
// };
|
| 68 |
-
// const handleClick = (event: MouseEvent) => {
|
| 69 |
-
// if (iframeRef?.current) {
|
| 70 |
-
// const iframeDocument = iframeRef.current.querySelector("iframe");
|
| 71 |
-
// if (iframeDocument) {
|
| 72 |
-
// const targetElement = event.target as HTMLElement;
|
| 73 |
-
// const iframeEl = iframeDocument as HTMLIFrameElement;
|
| 74 |
-
// const iframeBody = iframeEl.contentDocument?.body;
|
| 75 |
-
// if (targetElement !== iframeBody) {
|
| 76 |
-
// onClickElement?.(targetElement);
|
| 77 |
-
// }
|
| 78 |
-
// }
|
| 79 |
-
// }
|
| 80 |
-
// };
|
| 81 |
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
// iframeDocument.removeEventListener("click", handleClick);
|
| 90 |
-
// }
|
| 91 |
-
// }
|
| 92 |
-
// };
|
| 93 |
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
|
|
|
|
|
|
| 106 |
|
| 107 |
-
|
| 108 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
|
| 110 |
const selectedElement = useMemo(() => {
|
| 111 |
if (!isEditableModeEnabled) return null;
|
|
@@ -113,12 +146,7 @@ export const Preview = ({
|
|
| 113 |
return hoveredElement;
|
| 114 |
}, [hoveredElement, isEditableModeEnabled]);
|
| 115 |
|
| 116 |
-
const
|
| 117 |
-
return pages.reduce((acc, page) => {
|
| 118 |
-
acc[page.path] = page.html;
|
| 119 |
-
return acc;
|
| 120 |
-
}, {} as Record<string, string>);
|
| 121 |
-
}, [pages]);
|
| 122 |
|
| 123 |
return (
|
| 124 |
<div
|
|
@@ -166,11 +194,12 @@ export const Preview = ({
|
|
| 166 |
</span>
|
| 167 |
</div>
|
| 168 |
)}
|
| 169 |
-
<
|
| 170 |
id="preview-iframe"
|
|
|
|
| 171 |
title="output"
|
| 172 |
className={classNames(
|
| 173 |
-
"w-full select-none transition-all duration-200 bg-black h-full
|
| 174 |
{
|
| 175 |
"pointer-events-none": isResizing || isAiWorking,
|
| 176 |
"lg:max-w-md lg:mx-auto lg:!rounded-[42px] lg:border-[8px] lg:border-neutral-700 lg:shadow-2xl lg:h-[80dvh] lg:max-h-[996px]":
|
|
@@ -179,56 +208,25 @@ export const Preview = ({
|
|
| 179 |
currentTab !== "preview" && device === "desktop",
|
| 180 |
}
|
| 181 |
)}
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
"
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
|
|
|
| 200 |
</div>
|
| 201 |
);
|
| 202 |
};
|
| 203 |
-
|
| 204 |
-
const SandpackPreviewClient = ({
|
| 205 |
-
ref,
|
| 206 |
-
}: {
|
| 207 |
-
ref: React.RefObject<SandpackPreviewRef | null>;
|
| 208 |
-
}) => {
|
| 209 |
-
const { sandpack } = useSandpack();
|
| 210 |
-
|
| 211 |
-
useUpdateEffect(() => {
|
| 212 |
-
const client = ref.current?.getClient();
|
| 213 |
-
const clientId = ref.current?.clientId;
|
| 214 |
-
|
| 215 |
-
if (client && clientId) {
|
| 216 |
-
// console.log({ client });
|
| 217 |
-
// console.log(sandpack.clients[clientId]);
|
| 218 |
-
// const iframe = client.iframe;
|
| 219 |
-
// console.log(iframe.contentWindow);
|
| 220 |
-
}
|
| 221 |
-
/**
|
| 222 |
-
* NOTE: In order to make sure that the client will be available
|
| 223 |
-
* use the whole `sandpack` object as a dependency.
|
| 224 |
-
*/
|
| 225 |
-
}, [sandpack]);
|
| 226 |
-
|
| 227 |
-
return (
|
| 228 |
-
<SandpackPreview
|
| 229 |
-
ref={ref}
|
| 230 |
-
showRefreshButton={false}
|
| 231 |
-
showOpenInCodeSandbox={false}
|
| 232 |
-
/>
|
| 233 |
-
);
|
| 234 |
-
};
|
|
|
|
| 3 |
import { useMemo, useState } from "react";
|
| 4 |
import classNames from "classnames";
|
| 5 |
import { toast } from "sonner";
|
| 6 |
+
import { useThrottleFn } from "react-use";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
import { cn } from "@/lib/utils";
|
| 9 |
import { GridPattern } from "@/components/magic-ui/grid-pattern";
|
|
|
|
| 11 |
import { Page } from "@/types";
|
| 12 |
|
| 13 |
export const Preview = ({
|
| 14 |
+
html,
|
| 15 |
isResizing,
|
| 16 |
isAiWorking,
|
| 17 |
ref,
|
|
|
|
| 19 |
currentTab,
|
| 20 |
iframeRef,
|
| 21 |
pages,
|
| 22 |
+
setCurrentPage,
|
|
|
|
| 23 |
isEditableModeEnabled,
|
| 24 |
+
onClickElement,
|
| 25 |
+
}: {
|
| 26 |
html: string;
|
| 27 |
isResizing: boolean;
|
| 28 |
isAiWorking: boolean;
|
| 29 |
pages: Page[];
|
|
|
|
| 30 |
setCurrentPage: React.Dispatch<React.SetStateAction<string>>;
|
| 31 |
ref: React.RefObject<HTMLDivElement | null>;
|
| 32 |
+
iframeRef?: React.RefObject<HTMLIFrameElement | null>;
|
| 33 |
device: "desktop" | "mobile";
|
| 34 |
currentTab: string;
|
| 35 |
isEditableModeEnabled?: boolean;
|
| 36 |
onClickElement?: (element: HTMLElement) => void;
|
| 37 |
}) => {
|
| 38 |
+
const [hoveredElement, setHoveredElement] = useState<HTMLElement | null>(
|
| 39 |
+
null
|
| 40 |
+
);
|
| 41 |
+
|
| 42 |
+
const handleMouseOver = (event: MouseEvent) => {
|
| 43 |
+
if (iframeRef?.current) {
|
| 44 |
+
const iframeDocument = iframeRef.current.contentDocument;
|
| 45 |
+
if (iframeDocument) {
|
| 46 |
+
const targetElement = event.target as HTMLElement;
|
| 47 |
+
if (
|
| 48 |
+
hoveredElement !== targetElement &&
|
| 49 |
+
targetElement !== iframeDocument.body
|
| 50 |
+
) {
|
| 51 |
+
setHoveredElement(targetElement);
|
| 52 |
+
targetElement.classList.add("hovered-element");
|
| 53 |
+
} else {
|
| 54 |
+
return setHoveredElement(null);
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
};
|
| 59 |
+
const handleMouseOut = () => {
|
| 60 |
+
setHoveredElement(null);
|
| 61 |
+
};
|
| 62 |
+
const handleClick = (event: MouseEvent) => {
|
| 63 |
+
if (iframeRef?.current) {
|
| 64 |
+
const iframeDocument = iframeRef.current.contentDocument;
|
| 65 |
+
if (iframeDocument) {
|
| 66 |
+
const targetElement = event.target as HTMLElement;
|
| 67 |
+
if (targetElement !== iframeDocument.body) {
|
| 68 |
+
onClickElement?.(targetElement);
|
| 69 |
+
}
|
| 70 |
+
}
|
| 71 |
+
}
|
| 72 |
+
};
|
| 73 |
+
const handleCustomNavigation = (event: MouseEvent) => {
|
| 74 |
+
if (iframeRef?.current) {
|
| 75 |
+
const iframeDocument = iframeRef.current.contentDocument;
|
| 76 |
+
if (iframeDocument) {
|
| 77 |
+
const findClosestAnchor = (
|
| 78 |
+
element: HTMLElement
|
| 79 |
+
): HTMLAnchorElement | null => {
|
| 80 |
+
let current = element;
|
| 81 |
+
while (current && current !== iframeDocument.body) {
|
| 82 |
+
if (current.tagName === "A") {
|
| 83 |
+
return current as HTMLAnchorElement;
|
| 84 |
+
}
|
| 85 |
+
current = current.parentElement as HTMLElement;
|
| 86 |
+
}
|
| 87 |
+
return null;
|
| 88 |
+
};
|
| 89 |
+
|
| 90 |
+
const anchorElement = findClosestAnchor(event.target as HTMLElement);
|
| 91 |
|
| 92 |
+
if (anchorElement) {
|
| 93 |
+
let href = anchorElement.getAttribute("href");
|
| 94 |
+
if (href) {
|
| 95 |
+
event.stopPropagation();
|
| 96 |
+
event.preventDefault();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
|
| 98 |
+
if (href.includes("#") && !href.includes(".html")) {
|
| 99 |
+
const targetElement = iframeDocument.querySelector(href);
|
| 100 |
+
if (targetElement) {
|
| 101 |
+
targetElement.scrollIntoView({ behavior: "smooth" });
|
| 102 |
+
}
|
| 103 |
+
return;
|
| 104 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
|
| 106 |
+
href = href.split(".html")[0] + ".html";
|
| 107 |
+
const isPageExist = pages.some((page) => page.path === href);
|
| 108 |
+
if (isPageExist) {
|
| 109 |
+
setCurrentPage(href);
|
| 110 |
+
}
|
| 111 |
+
}
|
| 112 |
+
}
|
| 113 |
+
}
|
| 114 |
+
}
|
| 115 |
+
};
|
| 116 |
|
| 117 |
+
useUpdateEffect(() => {
|
| 118 |
+
const cleanupListeners = () => {
|
| 119 |
+
if (iframeRef?.current?.contentDocument) {
|
| 120 |
+
const iframeDocument = iframeRef.current.contentDocument;
|
| 121 |
+
iframeDocument.removeEventListener("mouseover", handleMouseOver);
|
| 122 |
+
iframeDocument.removeEventListener("mouseout", handleMouseOut);
|
| 123 |
+
iframeDocument.removeEventListener("click", handleClick);
|
| 124 |
+
}
|
| 125 |
+
};
|
| 126 |
|
| 127 |
+
if (iframeRef?.current) {
|
| 128 |
+
const iframeDocument = iframeRef.current.contentDocument;
|
| 129 |
+
if (iframeDocument) {
|
| 130 |
+
cleanupListeners();
|
| 131 |
+
|
| 132 |
+
if (isEditableModeEnabled) {
|
| 133 |
+
iframeDocument.addEventListener("mouseover", handleMouseOver);
|
| 134 |
+
iframeDocument.addEventListener("mouseout", handleMouseOut);
|
| 135 |
+
iframeDocument.addEventListener("click", handleClick);
|
| 136 |
+
}
|
| 137 |
+
}
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
return cleanupListeners;
|
| 141 |
+
}, [iframeRef, isEditableModeEnabled]);
|
| 142 |
|
| 143 |
const selectedElement = useMemo(() => {
|
| 144 |
if (!isEditableModeEnabled) return null;
|
|
|
|
| 146 |
return hoveredElement;
|
| 147 |
}, [hoveredElement, isEditableModeEnabled]);
|
| 148 |
|
| 149 |
+
const throttledHtml = useThrottleFn((html) => html, 1000, [html]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
|
| 151 |
return (
|
| 152 |
<div
|
|
|
|
| 194 |
</span>
|
| 195 |
</div>
|
| 196 |
)}
|
| 197 |
+
<iframe
|
| 198 |
id="preview-iframe"
|
| 199 |
+
ref={iframeRef}
|
| 200 |
title="output"
|
| 201 |
className={classNames(
|
| 202 |
+
"w-full select-none transition-all duration-200 bg-black h-full",
|
| 203 |
{
|
| 204 |
"pointer-events-none": isResizing || isAiWorking,
|
| 205 |
"lg:max-w-md lg:mx-auto lg:!rounded-[42px] lg:border-[8px] lg:border-neutral-700 lg:shadow-2xl lg:h-[80dvh] lg:max-h-[996px]":
|
|
|
|
| 208 |
currentTab !== "preview" && device === "desktop",
|
| 209 |
}
|
| 210 |
)}
|
| 211 |
+
srcDoc={throttledHtml as string}
|
| 212 |
+
onLoad={() => {
|
| 213 |
+
if (iframeRef?.current?.contentWindow?.document?.body) {
|
| 214 |
+
iframeRef.current.contentWindow.document.body.scrollIntoView({
|
| 215 |
+
block: isAiWorking ? "end" : "start",
|
| 216 |
+
inline: "nearest",
|
| 217 |
+
behavior: isAiWorking ? "instant" : "smooth",
|
| 218 |
+
});
|
| 219 |
+
}
|
| 220 |
+
// add event listener to all links in the iframe to handle navigation
|
| 221 |
+
if (iframeRef?.current?.contentWindow?.document) {
|
| 222 |
+
const links =
|
| 223 |
+
iframeRef.current.contentWindow.document.querySelectorAll("a");
|
| 224 |
+
links.forEach((link) => {
|
| 225 |
+
link.addEventListener("click", handleCustomNavigation);
|
| 226 |
+
});
|
| 227 |
+
}
|
| 228 |
+
}}
|
| 229 |
+
/>
|
| 230 |
</div>
|
| 231 |
);
|
| 232 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|