diff --git a/.eslintrc.js b/.eslintrc.js index 00e8e5e8..da41e112 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -48,7 +48,6 @@ module.exports = { "no-prototype-builtins": "off", "react/display-name": "off", "linebreak-style": ["error", "unix"], - quotes: ["error", "double"], semi: ["error", "always"], "semi-spacing": ["error"], "no-var": ["error"], diff --git a/src/components/display/FileDisplay.tsx b/src/components/display/FileDisplay.tsx index 4eaf3a83..47700957 100644 --- a/src/components/display/FileDisplay.tsx +++ b/src/components/display/FileDisplay.tsx @@ -25,13 +25,14 @@ import type { JSONType } from "@/types/json"; import AudioDisplay from "./AudioDisplay"; import CsvDisplay from "./CsvDisplay"; +import DocxDisplay from "./DocxDisplay"; +import HtmlDisplay from "./HtmlDisplay"; import ImageBlobDisplay from "./ImageBlobDisplay"; import JsonDisplay from "./JsonDisplay"; -import VideoDisplay from "./VideoDisplay"; -import XlsxDisplay from "./XlsxDisplay"; import MarkdownDisplay from "./MarkdownDisplay"; -import DocxDisplay from "./DocxDisplay"; import PdfDisplay from "./PdfDisplay"; +import VideoDisplay from "./VideoDisplay"; +import XlsxDisplay from "./XlsxDisplay"; import type { BlobDisplayProps } from "./types"; SyntaxHighlighter.registerLanguage("bash", bash); @@ -85,6 +86,7 @@ export const VIEWABLE_FILE_EXTENSIONS = [ // Documents "docx", + "html", "pdf", // Tabular data @@ -260,6 +262,8 @@ const FileDisplay = ({ uri, fileName, loading }: FileDisplayProps) => { return ; } else if (fileExt === "json") { return ; + } else if (fileExt === "html") { + return ; } else { return ; } diff --git a/src/components/display/HtmlDisplay.tsx b/src/components/display/HtmlDisplay.tsx new file mode 100644 index 00000000..9fe4a280 --- /dev/null +++ b/src/components/display/HtmlDisplay.tsx @@ -0,0 +1,80 @@ +import { useEffect, useRef, useState } from "react"; +import { Spin } from "antd"; + +import type { BlobDisplayProps } from "./types"; +import { LoadingOutlined } from "@ant-design/icons"; + +const HtmlDisplay = ({ contents, loading }: BlobDisplayProps) => { + const iframeRef = useRef(null); + + const [isConverting, setIsConverting] = useState(false); + const [iframeLoading, setIframeLoading] = useState(false); + + // Effect used to define the iframe load listener used to turn off the iframe loading state after the iframe has + // finished loading the specified srcdoc (derived from contents.) + useEffect(() => { + const iframe = iframeRef.current; + if (!iframe) return; + const listener = () => { + setIframeLoading(false); + }; + iframe.addEventListener("load", listener); + return () => iframe.removeEventListener("load", listener); + }, []); + + useEffect(() => { + const iframe = iframeRef.current; + + if (!iframe) return; + if (!contents) return; + + setIsConverting(true); + + contents + .text() + .then((html) => { + // If there isn't already a tag, instruct the embedded HTML to open links in the parent (i.e., here) + // instead of inside the iframe. + let modifiedHtml = html; + if (!modifiedHtml.includes("", ''); + } + + // When we update the srcdoc attribute, it'll cause the iframe to re-load the content. + // When it finishes loading, it'll trigger a load event listener on the iframe, defined above in another + // useEffect, which turns iframeLoading back off. This process lets us render a loading indicator. + iframe?.setAttribute("srcdoc", modifiedHtml); + setIframeLoading(true); + }) + .finally(() => setIsConverting(false)); + }, [contents]); + + // Three different loading states: + // - loading bytes from server + // - converting bytes to text (essentially instant) + // - iframe loading/rendering HTML - triggered by updating the srcdoc attribute + const isLoading = loading || isConverting || iframeLoading; + + return ( + }> + + + ); +}; + +export default HtmlDisplay;