Pomax / react-onclickoutside

An onClickOutside wrapper for React components
MIT License
1.83k stars 187 forks source link

Support for iFrame #236

Open rmdort opened 7 years ago

rmdort commented 7 years ago

If the react component is in an iframe, onclickoutside doesnt really work. You have to click on the parent document to trigger handleClickOutside

This module binds all click events to document. If you can expose the document prop, i can bind events to an iframe document instead. What do you think?

Andarist commented 7 years ago

Could you describe your issue in more detail? Im using this library in an iframe and it work just fine (within iframe ofc)

rmdort commented 7 years ago

Here is a plunkr demo https://plnkr.co/edit/1DwrgrAqkakmGMamJrXr?p=preview

When you click inside the iframe, handleClickOutside doesnt get called.

https://www.evernote.com/l/ABot4Su8DvBOSruUVClLgGWoUmq3Aywe5Aw

Note: I am using react-iframe-component - https://github.com/ryanseddon/react-frame-component

Pomax commented 7 years ago

This is how browsers work: parent pages do not get notified of clicks inside iframes because those are literally different pages with different security domains, and knowing anything about what happens inside of them means leaking information that can be used for exploits.

Aside from obviously not using iframes (except for maybe editor live-views, they're almost never a good thing), the only signal you get when you click inside the iframe is that the parent document is blurred. However, as the parent can get blurred for a variety of reasons (for instance, clicking outside the browser) that is not a signal that can be used to determine that an outside click inside the same document (as far as the user experiences documents) occurs.

What you can do, if you control both the parent and the iframed content, and you absolutely need this, is write your some code that makes sure that your parent page and iframed page know how to communicate using postMessage or the like, so that iframed page(s) can signal "something happened inside of me that you wanted to hear about", and then make the parent manually trigger the outside click when it hears about something that you deem should trigger outside-click behaviour.

Andarist commented 7 years ago

Aside from obviously not using iframes (except for maybe editor live-views, they're almost never a good thing)

actually iframes are still the safest bet for installable 3rd party widgets too

I think @rmdort might have something else on mind - I think he might be in control of both parent document and iframe and he renders into the iframe from the parent document, so the outside click handler gets registered at the moment in the parent document, while he wants it to be registered inside. Is that right, @rmdort ?

rmdort commented 7 years ago

@Andarist Yes. We are using iframes for widgets. I will have control of the parent page as the Javascript to render the widget has to be in the parent page.

@Pomax I will have multiple onclickOutside wrapped react components in the Widget. So even if i use postMessage or add a click listener on the parent page, how do i get the correct handleClickOutside handler to call ?

Since you are binding all touch, mousedown events on the document, why cant i instead attach the events to IFRAME.documentElement . Wouldnt it work?

Cant we expose the document prop so i can pass any element for binding ?

Pomax commented 7 years ago

the postmessage design is fully up to you, so one way would be to make sure all your widgets post a message that is JSON serialized data like:

JSON.stringify({
  type: "mynamespace::clickedOnWidget",
  source: "iframe identifier of some sort that the iframe knows about because you passed it in through the iframe's href as a query argument"
})

That way the parent document can check incoming post messages, and if it's the right type, it can simulate a click event with mouse coordinates (or touch coordiantes) that correspond to the iframe's document coordinates and no additional work on your end would be required: the click event would get seen by the HOC and because the source is not whatever menu is open right now, that menu will close.

(you can also do source binding by adding a negotiation step to the postmessage communication system, where you bootstrap your iframe with a one-time-pad password as url argument that it can send back to the parent so that the parent can go "I just got a postmessage from source X that uses the right type, and uses the random password I made up for it: I will add this soucrce to my list of whitelisted sources and now I will allow it to send "real" messages to me")

Andarist commented 7 years ago

He would still have to detect somehow clicks inside the iframe, but outside of the wrapped component (the one in the iframe) - I think thats why configurable documentElement is requested here.

Pomax commented 7 years ago

the idea is that the iframe listens for clicks at its own document level, then sends a postMessage signal to the parent that "a click has occurred inside of me", so that the parent can then dispatch an artificial click event on its own document object, which'll automatically trigger the outside click handler.

If a document needs to do anything based on something happening inside of an iframe embedded in the document, then a mutually agreed communication channel needs to be set up, and agreed upon signals need to be sent from one to the other (you could probably even use a websocket connection, although that's a little heavier-handed than just using a decent set of messages through postmessage)

rmdort commented 7 years ago

I am not sure we are on the same page here.

In my example, the iframe is not listening for click in the document level. Instead the parent page is listening for clicks. The plugin is attaching event handlers to the parent document.

Instead if the plugin attaches click event to iframe's document, it will all just work.

Note: I don't want to trigger handleClickOutside when the parent page is clicked. I am expecting to trigger the handler when user clicks on the iframe. ie anywhere outside the element, but still inside the iframe

Pomax commented 7 years ago

No, we're talking about the same thing: if you have a setup with page X containing an iFrame Y, then for security reasons X is never notified of events originating in Y. When you click anywhere inside the clientRect for Y, Y sees that click and is allowed to handle it, but X will never be notified of that click because that would be leaking potentially exploitable information.

Specifically to this HOC, mouse events that occur because of something you do while your cursor (or finger) is over the iFrame will never trigger event handling in the parent (whether you use React or plain Javascript), and the only way around that is for you to intentionally set up a communication channel between X and Y (postmessage, websocket, polling localStorage or sessionStorage, a nasty bit of sneaky ninja Flash for out-of-browser storage, what have you), so that you can make sure that there is code in place that effects events in Y turn into signals that are sent to X for proper handling.

Andarist commented 7 years ago

@Pomax I think @rmdort is right here - he describes a little bit different scenario than you.

He has page X which creates components (including the one wrapped in our HOC) and which creates src-less iframe Y - there are no security concerns here and page X can manipulate iframe's Y content and the opposite it true too.

Actually he doesn't want any support for listening to click documents from both documents - he just want simply be able to listen to click events of frame Y where he renders his wrapped component.

At the moment component rendered into frame Y is listening to page's X events.

rmdort commented 7 years ago

Yes :+1

rmdort commented 6 years ago

Where i have gotten so far is add document variable to my context, which points to my iframe document

const MyComponent = () => {}
MyComponent.contextTypes = {
   document: PropTypes.object
}

var clickConfig = {
  getDocument: function (instance) {
    return instance.context.document // This can be any user defined function
  }
}
export default listensToClickOutside(MyComponent, clickConfig)

And replace document references in the plugin to getDocument function, if exists, else fallbacks to window.document. If you are fine with this, i can submit a pull request.

rmdort commented 6 years ago

Added a pull request #255 . Can you review it

rosa-nugget commented 6 years ago

Is there somethings in react-onclickoutside to support what @Pomax is talking about? Page X have a React component wrapped in react-onclickoutside HOC and a iframe Y, the React component is not inside of the iframe, and obviously the React component did not respond to the onclickoutside event when click on iframe. Do you recommend using some type of communication(postmessage) with the iframe? I am looking for a solution and I found 2 other different solutions like creating a modal-backdrop or adding $(window).on('blur',function() { ... } ); , I am trying to analyze this different options.