react-pdf-viewer icon indicating copy to clipboard operation
react-pdf-viewer copied to clipboard

No-op memory leak inside the CanvasLayer

Open LucaHermann opened this issue 4 years ago • 3 comments

Image from Gyazo

LucaHermann avatar Oct 19 '21 12:10 LucaHermann

Thanks for reporting the issue, @LucaHermann Are there any steps that I can reproduce the issue? Can you fork this sample project?

phuocng avatar Oct 19 '21 13:10 phuocng

I basically every-time a PDF is open on the platform.

import React, { useState, useEffect, useRef } from "react"
import { useSelector, useDispatch } from "react-redux"
import { navigate, useLocation } from "@reach/router"
import ReactDOMServer from "react-dom/server"
import { alpha } from "@material-ui/core/styles"
import { Box, makeStyles } from "@material-ui/core"
import "@react-pdf-viewer/core/lib/styles/index.css"
import "@react-pdf-viewer/zoom/lib/styles/index.css"
import { zoomPlugin } from "@react-pdf-viewer/zoom"
import { Viewer, Worker } from "@react-pdf-viewer/core"

import Topbar from "./Topbar"
import Sidebar from "./Sidebar"
import { useQuery } from "../../../utils/hooks"
import { documentDownloadData } from "../../../actions/document"
import packageJson from "../../../../package.json"

// put the documentPreview query in the URL to trigger the opening of the DocumentPreview
export const openDocumentPreview = (documentID, navState) => {
  let url = window.location.pathname
  if (window.location.search) {
    // look if documentPreview is already present and replace it with the new one
    const documentPreviewRegexp = /documentPreview=[0-9]+/
    if (documentPreviewRegexp.test(window.location.search))
      url = window.location.search.replace(documentPreviewRegexp, `documentPreview=${documentID}`)
    else
      url += `${window.location.search}&documentPreview=${documentID}`
  }
  else
    url += `?documentPreview=${documentID}`
  if (navState)
    return navigate(url, { state: navState })
  navigate(url)
}

const HighlightText = ({ children }) => <span style={{
  backgroundColor: alpha("#FF00", 1),
}} className="_preview-highlight">
  {children}
</span>

export default function DocumentPreview({ documentID }) {
  const classes = useStyles()
  const dispatch = useDispatch()
  const { applyDataroomFilters } = useQuery()
  const { state: navState } = useLocation()
  // get it from store to have the last updated version
  const document = useSelector(state => state.documents.find(e => e.id === documentID))
  const [viewer, setViewer] = useState({ type: null, data: null })
  const [highlights, setHighlights] = useState([])
  const topbarRef = useRef()
  const pdfRef = useRef()
  const windowWidth = useRef(0)

  const zoomPluginInstance = zoomPlugin()
  const { ZoomInButton, ZoomOutButton, ZoomPopover } = zoomPluginInstance

  // put it here as there is some side effect when putting in a useEffect (missing height)
  const viewerHeight = window.innerHeight - topbarRef.current?.clientHeight || 0
  const viewerWidth = Math.round(windowWidth.current * 0.7)

  useEffect(() => {
    windowWidth.current = window.innerWidth
  }, [])

  // TODO UX: see if we had a snack content in case the user hasn't acess
  useEffect(() => {
    if (documentID && !document)
      return navigate(window.location.pathname)
  }, [documentID, document])

  useEffect(() => {
    if (document && document.content_type === ".tinymce")
      setViewer({ type: "html", data: document.html_content })
    else if (document && !document.blobData)
      dispatch(documentDownloadData(documentID, true))
  }, [document, document?.content_type, document?.blobData, documentID, dispatch])

  useEffect(() => {
    if (document?.blobData) {
      let reader = new FileReader()
      reader.readAsArrayBuffer(document.blobData)
      reader.onloadend = function () {
        setViewer({ type: "pdf", data: new Uint8Array(reader.result) })
      }
    }
  }, [document, document?.blobData])

  const removeOccurencies = () => {
    let occurencies = [...window.document.getElementsByClassName("_preview-highlight")]
    occurencies.forEach(occurency => occurency.offsetParent.innerHTML = occurency.offsetParent.innerText)
    setHighlights([])
  }

  const findOccurrence = (str) => {
    removeOccurencies()
    if (str === "N/A")
      return
    const regexp = new RegExp(str, "i")
    // get the div where every pages ar rerender individually
    const innerPages = [...pdfRef.current.children[0].children[0].children[0].children]
    let occurences = []
    innerPages.forEach(page => {
      // access the div where are located avery span who contain piece of text from the pdf
      // if not accessible increase the amount of time in the timeout in documentLoad func
      const textSpans = [...page.children[0].children[1]?.children || []]
      if (textSpans) {
        textSpans.forEach(item => {
          // apply highlight
          if (regexp.test(item.innerHTML)) {
            item.innerHTML = item.innerHTML.replace(regexp,
              (correspondance) => ReactDOMServer.renderToStaticMarkup(<HighlightText>{correspondance}</HighlightText>))
            occurences.push(item)
          }
        })
      }
    })
    if (occurences.length) {
      // scroll to the first one
      let innerPage = pdfRef.current.children[0].children[0].children[0]
      setHighlights(occurences)
      let topHeight = occurences[0].getBoundingClientRect().top
      // we add +300 for better UX, if the DOM change, change it too
      innerPage.scrollTop += (topHeight - 300)
    }
  }

  // go the the next occurence based on a GOOD idx
  const nextOccurence = (idx) => {
    if (!highlights.length)
      return
    let innerPage = pdfRef.current.children[0].children[0].children[0]
    let nextOccurence = highlights[idx]
    let topHeight = nextOccurence.getBoundingClientRect().top
    // we add +300 for better UX, if the DOM change, change it too
    innerPage.scrollTop += (topHeight - 300)
  }

  // reset the URL without the documentPreview query
  const handleClose = () => {
    let nextUrl = window.location.pathname
    if (applyDataroomFilters)
      nextUrl += `?applyDataroomFilters=${applyDataroomFilters}`
    navigate(nextUrl, { state: { navbar: navState?.navbar } })
  }

  const onDocumentLoad = () => {
    // to force the render of each page in the DOM
    // (100ms seems to be OK, less can be not sufficient on large pdf file)
    let pageDiv = pdfRef.current.children[0].children[0].children[0]
    pageDiv.scrollTop = pageDiv.scrollHeight
    setTimeout(() => {
      pageDiv.scrollTop = 0
    }, 200)
  }

  if (!document)
    return null
  return (
    <>
      <div ref={topbarRef}>
        <Topbar handleClose={handleClose} document={document} />
      </div>
      {viewerHeight > 0 && windowWidth.current > 0 &&
        <Box display="flex">
          <div style={{
            padding: 24,
            height: viewerHeight,
            width: viewerWidth,
            minWidth: viewerWidth,
            backgroundColor: "grey",
          }}>
            {viewer.type === "pdf" ?
              <Worker workerUrl={`https://unpkg.com/pdfjs-dist@${packageJson.dependencies["pdfjs-dist"]}/build/pdf.worker.min.js`}>
                <div
                  className="rpv-core__viewer"
                  style={{
                    border: "1px solid rgba(0, 0, 0, 0.3)",
                    display: "flex",
                    flexDirection: "column",
                    height: "100%",
                  }}>
                  <div className={classes.viewerContainer}>
                    <ZoomOutButton />
                    <ZoomPopover />
                    <ZoomInButton />
                  </div>
                  <div ref={pdfRef} style={{ flex: 1, overflow: "hidden" }}>
                    <Viewer onDocumentLoad={onDocumentLoad} fileUrl={viewer.data}
                      plugins={[zoomPluginInstance]} />
                  </div>
                </div>
              </Worker>
              :
              <div className={classes.innerHtml}
                dangerouslySetInnerHTML={{ __html: viewer.data }} />
            }
          </div>
          <div style={{
            height: viewerHeight,
            flexGrow: 1,
            overflowY: "auto",
          }}>
            <Sidebar document={document}
              highlights={{ findOccurrence, nextOccurence, foundOccurences: highlights }} />
          </div>
        </Box>
      }
    </>
  )
}

const useStyles = makeStyles(() => ({
  innerHtml: {
    backgroundColor: "white",
    width: "100%",
    height: "100%",
    padding: 10,
    overflow: "auto",
    "& p": {
      marginTop: 0,
    }, "& h1": {
      marginTop: 0,
    }, "& h2": {
      marginTop: 0,
    }, "& h3": {
      marginTop: 0,
    }, "& h4": {
      marginTop: 0,
    }, "& h5": {
      marginTop: 0,
    }, "& h6": {
      marginTop: 0,
    }, "& pre": {
      marginTop: 0,
    },
  },
  viewerContainer: {
    alignItems: "center",
    backgroundColor: "#eeeeee",
    borderBottom: "1px solid rgba(0, 0, 0, 0.1)",
    display: "flex",
    justifyContent: "center",
    padding: "4px",
  },
}))

LucaHermann avatar Oct 19 '21 13:10 LucaHermann

Please create a minimal project that I can reproduce the issue. Otherwise, there is not much I can help.

phuocng avatar Oct 19 '21 13:10 phuocng