Flyer53 / jsPanel4

A JavaScript library to create highly configurable floating panels, modals, tooltips, hints/notifiers/alerts or contextmenus for use in backend solutions and other web applications.
https://jspanel.de/
Other
313 stars 57 forks source link

iframe overlay bug and enhancement #79

Closed fq-selbach closed 5 years ago

fq-selbach commented 5 years ago

Expected behavior: Opening an iframe and another panel next to each other and then focusing the normal panel will put an "jsPanel-iframe-overlay" over the iframe to block the first click into the panel so that it only brings the panel to the front.

Observed behavior: The 'jsPanel-iframe-overlay' will be generated multiple times and each of the overlays requires an additional click to remove them again. I was able to reproduce this by simply clicking the 'other' panel multiple times and for each click a new overlay was added to the iframe panel.

Kind request (or question): For my use-case the overlay is very annoying and gives a confusing user-experience (probably mainly due to the bug). Is it possible to deactivate the 'feature' altogether maybe using an option entry? For now I've removed this part of the code in jsPanel.js: https://github.com/Flyer53/jsPanel4/blob/d9cd1890ecba72932cb114ef4061e1774f8f7cd0/dist/jspanel.js#L1286

fq-selbach commented 5 years ago

Update: ok I've just realized why this overlay is necessary ^^. If you drag a panel it can easily interfere with the iframe and lock you down in e.g. resize mode 😬. Can we make this "safety" overlays activate only while resize or drag is active?

Flyer53 commented 5 years ago

This safety overlay is only created within the front() method. Options dragit/resizeit of a panel just set the content sections CSS prop pointer-events to 'none' while dragging/resizing. pointer-events: 'none' doesn't work for the front method because you then couldn't front the panel with a click on the iframe. So all panels with an iframe except the topmost one (z-index) will have the overlay and might require an extra click to remove the overlay in order to make the content accessible.

Multiple safety overlays is a bug of course. Probably the reason why the overlays appeared to be related to dragging/resizing.

I updated the 4.6.0-alpha.1 download with a fix in the front() method preventing multiple overlays. That also removes the overlay on dragstart/resizestart. I didn't change the version number though ... so just download again and let me know how it works out now please.

Note: The overlay of the front() method might not be the most perfect solution. But I couldn't find a better one yet.

fq-selbach commented 5 years ago

Thanks a lot for the great support! I can confirm that the new version has the multiple-overlays bug fixed :-) 👍 👍 I actually did not notice that the iframe panel was not coming back to front because I have them next to each other. Besides the "bring-to-front" issue the overlay also seems to make it much safer when you resize a panel that is on top of the iframe panel (I think interaction with the iframe breaks something without the overlay).

I still wonder if there is any way to make the click on a button happen without clicking twice (one to bring to front, one to activate button). Maybe one could buffer the click on the jsPanel-iframe-overlay and dispatch it again after it is brought to front? Something like:

var ev = new MouseEvent('click', {
    'view': window,
    'bubbles': true,
    'cancelable': true,
    'screenX': x,
    'screenY': y
});
panelWithIframe.dispatchEvent(ev);
Flyer53 commented 5 years ago

Thanks for the bug report 😃

... interaction with the iframe breaks something ...

The basic problem behind this is that we're dealing with two different documents. The first is ... let's call it the host document, and the second is the one in the iframe. Both of them have their own execution context. When you resize a panel and the mouse moves over an iframe the mouse enters the execution context of another document without a mouseup in the other document that would remove the resize handler.

For all I know about client side javascript (which is not that awfully much) there is no way to access/edit the js of one document from the other document. So your idea won't work because the first click fronting the panel (and removing the overlay) happens in the host document and the second click happens (and is handled) in the iframe.

Again, my js knowledge is far from complete and if you find something helpful ... I'd be glad to learn ... I hope my English is good enough to make the point 😏

EDIT: I might have an idea for you if

fq-selbach commented 5 years ago

Thanks for the explanation! My hope was that the mouse event might be passed down to the second document automatically by the browser :-( The conditions you mentioned at the end may apply sometimes but are not guaranteed unfortunately so the solution you had in mind might be a workaround for some situations at best, but ... ... I am injecting an interface into the iframe anyway to establish a two-way data connection so there might be a way to dispatch the mouse event from inside the iframe 🤔. Since I have no CORS issues (the iframe loads an application from the same origin as the panl interface) it might even be possible to simply use:

var iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
iframeDoc.dispatchEvent(ev);

Have you tried something like this?

Flyer53 commented 5 years ago

Well, something new learned again 😄 Found this https://www.dyn-web.com/tutorials/iframes/refs/iframe.php and created a simple example page.

Got to https://jspanel.de/docs/demos/trigger_click_in_iframe.html It loads one of the jsPanel API pages in an iframe. After a delay of 3secs it triggers a click to navigate to another topic and after yet another 3 secs it triggers a click on example 1.

The js in the doc containing the iframe:

document.getElementById('frame1').onload = function() {
    // reference to iframe with id 'frame1'
    var ifrm = document.getElementById('frame1');
    // using reference to iframe (frame1) obtained above
    var win = ifrm.contentWindow; // reference to iframe's window
    // reference to document in iframe
    var doc = ifrm.contentDocument ? ifrm.contentDocumen t: ifrm.contentWindow.document;

    setTimeout(function () {
        var event = new MouseEvent('click');
        var elmt = doc.querySelector('a[href="#options/animateIn"]');
        elmt.dispatchEvent(event);

        setTimeout(function () {
            var event = new MouseEvent('click');
            var elmt = doc.querySelector('#sample-animatein-1');
            elmt.dispatchEvent(event);
        }, 3000);

    }, 3000);
};

Of course it works only if iframe.src is on same domain!

fq-selbach commented 5 years ago

Great! 😁. I guess now we just have to figure out the correct mouse click coordinates inside the iframe or? (I usually get the wrong ones due to viewport/window/page/document/client confusion 🤦‍♂️).

Flyer53 commented 5 years ago

That won't be enough I guess. You still have to identify the element under the cursor in order to trigger a click on it.

fq-selbach commented 5 years ago

This you can do with var el = document.elementFromPoint(x, y); or var el = document.elementsFromPoint(x, y); (first one will give you the first element, second an array).

Flyer53 commented 5 years ago

Oh great 😄 Didn't know this methods yet ...

fq-selbach commented 5 years ago

I just noticed another issue with the iframe overlay 🙈. If the panel has a child panel that itself has an iframe the methods item.content.querySelector('.jsPanel-iframe-overlay') and item.content.querySelector('iframe') will find that iframe and assume it belongs to the parent 😬

[EDIT1]: I've tried to fix this assuming that an iframe has to be a direct child of the content object with:

//var overlay = item.content.querySelector('.jsPanel-iframe-overlay');
var overlay;
if (item.content.children){
    for (var i=0; i<item.content.children.length; i++){
        if (item.content.children[i].classList.contains('jsPanel-iframe-overlay')){
            overlay = item.content.children[i];
            break;
        }
    }
}
if (index > 0) {
    //if (item.content.querySelector('iframe') && !overlay) {
    if (item.content.firstElementChild && item.content.firstElementChild.tagName == "IFRAME" && !overlay) {

But this will not work either because if you press the child panel this will trigger 2 calls to the "front" method, first to itself and AFTERWARDS to the parent. The 2nd call will then add the overlay again.

Flyer53 commented 5 years ago

Copied ... I just updated to a final v4.6.0 on the jsPanel homepage this minute and the GitHub repo will follow today. Then I'd like to close those issues I consider fixed.

Quick tip: Maybe you can do something with item.content.querySelectorAll('iframe') and then go over each iframe individually.

Let me check that out later ... we might have to copy your latest finding to a new issue then. Okay?

Note: As of tomorrow night (March 28.) I'll be on vacation for about 2 weeks (doing some backcountry skiing 😄 without any means of communication other than a cellphone). So it'll take some time.

fq-selbach commented 5 years ago

kk :-) enjoy! I just updated to the new release version and implemented my 2 hacks for the other issues. I'll experiment a bit more with this overlay thingy, maybe I can find a clean fix ;-)

Flyer53 commented 5 years ago

So I close this issue for now and we'll check on the iframe particularities again in about 2 weeks ...

Happy coding 😏