Search code examples
javascriptreactjsiframe

React archive iframe onClick behaviour


I want to archive an onClick event on an iframe that I use to show different invoices pdf.

For that I use a filelist:

 const [filelist, setFilelist] = useState([])


  {filelist?.map((element, index) => (
                <div key={index} id={index + "_pdf"}
                    onMouseOver={(e) => handleOnMouseOver(e)}
                    onMouseOut={(e) => handleOnMouseOut(e)}
                >
                    {<iframe title="pdf_viewer" src={`${element.file_path_fe}`} width="100%" height="400em" />}
                </div>
            ))}

Now I've found this snippet:
https://codesandbox.io/p/sandbox/react-detect-mouse-click-over-iframe-708ys?file=%2Fsrc%2FApp.js%3A21%2C4
where clicks are detected via the blur effect. Since I use functional components, I have rewritten it this way:

useEffect(() => {
        ...

        // iframe clickable:
        // Focus the page
        window.focus();
        // Add listener to check when page is not focussed
        // (i.e. iframe is clicked into)
        window.addEventListener("blur", onblur);

        return () => {
            // Anything in here is fired on component unmount.
            // so clean up the window eventlistener
            window.removeEventListener("blur", onblur);
        }

        // remove onblur missing dependency warning
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []);

and the functions:

const onblur = (e) => {
        if (state) {
            //window.focus();
            console.log(e);
        }
    };

    const handleOnMouseOver = (e) => {
        console.log("mouse in");
        setState({ iframeMouseOver: true });
    };

    const handleOnMouseOut = (e) => {
        console.log("mouse out");
        window.focus(); // Make sure to set focus back to page
        setState({ iframeMouseOver: false });
    };

It works, but it's not exactly how I want it to work. So my questions would be:

  1. It detects the click correctly, but only one at a time. After that, clicks are ignored. I've tried to refocus the window in the hopes I can refire the blur eventListener on another click, but that doesn't work. What can I do to archive and track multiple clicks?
  2. How can I detect the current Iframe if I click on it? e.target still detects the window it seems. Since I have multiple pdf's, I would like to get the current pdf I've clicked on.
  3. Is it the correct approach to add the window.eventListener in the useEffect hook? Since there is the warning because of the onblur function.

Edit: I had an idea on how to track the current iframe, I totally forgot that I have the mouseOver events. I added a variable:

const [pdfFrame, setPdfFrame] = useState([])

and set the variable in the mouseOver events:

const handleOnMouseOver = (e) => {
    setPdfFrame([e.target])
    setState({ iframeMouseOver: true });
};

const handleOnMouseOut = (e) => {
    window.focus(); // Make sure to set focus back to page
    setPdfFrame([])
    setState({ iframeMouseOver: false });
};

but for some reason, the blur function always shows an empty variable, even though it's rerendered with the correct value:

console.log("Rendering with: ", pdfFrame);
const onblur = () => {
    if (state) {
        window.focus();
        console.log(pdfFrame);
    }
};

I think this would be the correct approach, but I miss something here.

Edit2: Thanks to Lanre I got it to work, here is the full component (without the irrelevant stuff):

import "./Matching.css"
import { useState, useEffect, React, useCallback } from 'react';

const Matching = () => {
    const [filelist, setFilelist] = useState([])
    const [state, setState] = useState([{ iframeMouseOver: false }])
    const [pdfFrame, setPdfFrame] = useState(null)

    const mandant = 2;


    useEffect(() => {
        /* here set the filelist, but only once on loading thus the []
           Create an file folder in public folder and add the pdf with this name(or any other name, and adjust the json)
           example:
        {
            "file_path": "./view/public/files/2/8dc9e7a05ad84f0bd675624416f7c308.pdf",
            "file_path_fe": "./files/2/8dc9e7a05ad84f0bd675624416f7c308.pdf",
            "hash": "8dc9e7a05ad84f0bd675624416f7c308",
            "id": 1,
            "mandant": "2",
            "name": "Satzung.pdf",
            "org_name": "Satzung.pdf",
            "size": 126286,
            "type": "application/pdf"
        }
        */
        axios.post("/select", {
            "table": "files",
            "columns": ["*"],
            "where": `where mandant = ${mandant}`
        }).then(res => setFilelist(res.data))
    }, [])

    //var markedRows = []
    const onblur = useCallback(() => {
        if (state.iframeMouseOver) {
            setTimeout(() => {
                window.focus();
            }, 0);
        }
    }, [pdfFrame, state.iframeMouseOver]);

    // componentDidMount equivalent
    useEffect(() => {
        // iframe clickable:
        // Focus the page
        window.focus();
        // Add listener to check when page is not focussed
        // (i.e. iframe is clicked into)
        window.addEventListener("blur", onblur);

        //console.log(pdfFrame);
        return () => {
            // Anything in here is fired on component unmount.
            // so clean up the window eventlistener
            window.removeEventListener("blur", onblur);
        }
    }, [onblur]);


    const handleOnMouseOver = (e) => {
        setPdfFrame(e.currentTarget.querySelector('iframe'))
        setState({ iframeMouseOver: true });
    };

    const handleOnMouseOut = (e) => {
        setPdfFrame(null)
        setState({ iframeMouseOver: false });
        window.focus(); // Make sure to set focus back to page
    };

    return (
        <div className="matching-wrapper">
            <div className="row">
                <div className="col-3">
                    {filelist?.map((element, index) => (
                        <div key={index} id={index + "_pdf"}
                            onMouseOver={(e) => handleOnMouseOver(e)}
                            onMouseOut={(e) => handleOnMouseOut(e)}
                        >
                            {<iframe title="pdf_viewer" src={`${element.file_path_fe}`} width="100%" height="400em" />}
                        </div>
                    ))}
                </div>
                <div className="col-2">
                    {/* other stuff not relevant to the question*/}
                </div>
                <div className="col-7">
                    {/* other stuff not relevant to the question*/}
                </div>
            </div>
        </div >
    );
};

export default Matching;

Solution

  • So I can suggest a couple of fixes that you could try. Looking through your code, I think you could make some changes like:

    1. Programmatically refocus the Window, something like:

    const onblur = () => {
        if (state.iframeMouseOver) {
            console.log("Iframe was clicked");
            setTimeout(() => {
                window.focus();
            }, 0);
        }
    };

    1. You can refine how you're detecting the current . Try this:

    const [pdfFrame, setPdfFrame] = useState(null);
    
    const handleOnMouseOver = (e) => {
        setPdfFrame(e.currentTarget.querySelector('iframe'));
        setState({ iframeMouseOver: true });
    };
    
    const handleOnMouseOut = (e) => {
        setPdfFrame(null);
        setState({ iframeMouseOver: false });
        window.focus();
    };

    1. You can use a callback to properly ensure onblur always has access to the current state and props too.

    const onblur = useCallback(() => {
        if (state.iframeMouseOver) {
            console.log("Iframe clicked:", pdfFrame);
            setTimeout(() => {
                window.focus();
            }, 0);
        }
    }, [pdfFrame, state.iframeMouseOver]);
    
    useEffect(() => {
        window.addEventListener("blur", onblur);
    
        return () => {
            window.removeEventListener("blur", onblur);
        };
    }, [onblur]);

    Let me know if this helps. Cheers!

    Update: @user3793935 has updated the question with the working solution.