-
Notifications
You must be signed in to change notification settings - Fork 2.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Reparenting iframes without reloading #5484
Comments
Some more clarification: right now the use case only involves moving iframes or other elements within the same document without ever actually detaching them. |
Node trees don't offer move as a primitive operation. |
Some people implemented this behavior in WebKit, found several critical issues, and removed it. |
Based on this patch which removed magic iframes, it sounds like they were for moving iframes between documents, which sounds more vulnerability prone than moving iframes inside of one document. Ryosuke's comment at the end of the issue makes it sound like there are also security vulnerabilities with moving iframes inside the same document, but I wonder if the security vulnerabilities they encountered were really for the same document cases... |
The most of security issues were agnostic to whether an iframe was moved across a document or not. The main issue is the document inside a (temporarily) disconnected iframe being considered alive. |
Thanks for the feedback Ryosuke! Do you think that it would be possible to move the iframe atomically and therefore never have a document in a disconnected frame? |
That was the idea. But how does one move a node from one place to another? Since a node can't have multiple parents, it needs to be disconnected at some point in some internal engine state. That's precisely what caused the problem. In particular, if there are multiple iframe that could get disconnected in order, then they can access each other in weird ways in scripts, etc... I'm not gonna go into details but I don't think this is an idea we should re-pursue anytime soon. |
Moving between parents would be very useful but the use case for React (and similar) is to be able to move position within the same parent. Eg swapping the place of two nodes in the same parent. |
@rniwa did you introduce a new type of mutation for node trees? It seems to me you would have to start there. We only have remove/insert at the moment. |
I don't recall but I don't think that really helps so long as such a replacement involves more than one node at once (e.g. subtree) because then a subtree can contain iframe which then need to be kept alive, etc... |
I think it would help in that you can define now rules around it in terms of script execution, but it would be a pretty large undertaking and there's a lot of non-interoperable behavior around this for the current remove/insert operations that should be sorted first. |
Rather than inventing a new kind of tree mutation, it feels like the simplest solution for developers would be to keep the iframe alive for a microtask after disconnection (although maybe need to think more about mutation events, as they already use microtasks). I realise this doesn't make implementation easier. |
How would that work specification-wise? Please note whatwg/dom#808 and ideally everything linked from there to get an idea about the complexity with regards to script execution that we have with insert and remove today. (Mutation events also fire synchronously for insert/remove and are also not specified yet and indeed also cause problems here.) |
I figured it'd be something like: An iframe has a pending discard flag which is unset. When an iframe element is removed from a document:
Although, if the iframe is inserted into another document, the current nested browsing context should also be discarded (if that was a particular source of security bugs). |
For what it's worth, from a web dev perspective, there's already a way to ask for an element to be moved in a single step: just call .appendChild or .insertBefore for its new position without ever calling .removeChild. This is in fact what React already does when reordering children within a list. |
I'd like to call out this part too:
The example of iframes is one particularly complicated case but it's one of several examples of state being lost. Text selection and focus within an CSS transitions playing in a subtree will be reset if any parent is removed and appended. These are much more common and problematic than iframes.
It might be worth while listing out all these cases, because I could very well imagine that it's not web compatible to change this behavior for all of them using appendChild only. That might motivate why a new API for moves is needed. This is a big deal in diffing based solutions (this include virtual DOMs like React and template based solutions based on diffing rather than change tracking). The reason it is such a big deal is that there's nothing that says which node to move. If you swap two nodes and diff them, there's nothing in the API to imply which one should be moved. The algorithm just picks one to move. Depending on the algorithm tradeoffs you could also end up moving four other nodes to move one since it's not worth computing the longest common subsequence. Any browser built-in diffing based approach will also need to consider this problem. In an imperative manual API you might have some intuition about which is safer and less disturbing to move. As such this was much less of a problem for the first decades of the web. However, even then there are cases where you just have to live with the worse experience. In React we have some code to work around cases such as restoring selection and focus which adds to the code size of React, but we leave some unsolved. |
@sophiebits unfortunately that's not an atomic move and will do a remove and then an insert (with all the associated script running bits). @sebmarkbage an exhaustive list of problematic moving scenarios would help a lot. |
And what happens when JS runs in the iframe while the iframe isn't really connected. How does window.parent in the iframe's window work, what should history.go() do, or location.href = "newurl"? How should window.frames behave in the parent window? ... |
Somewhat limited, but for what is worth, you can sorta get that effect with shadow DOM and slotting, it seems to work in all browsers: <!doctype html>
<button>Toggle</button>
<div id="host">
<iframe id="frame" src="https://wikipedia.org" slot="a" style="border: 0"></iframe>
</div>
<script>
document.getElementById("host").attachShadow({ mode: "open" }).innerHTML = `
<style>
div { height: 150px; background: blue; }
#b { background: purple }
</style>
<div id="a">
<slot name="a"></slot>
</div>
<div id="b">
<slot name="b"></slot>
</div>
`;
document.querySelector("button").addEventListener("click", function() {
let frame = document.getElementById("frame");
frame.slot = frame.slot == "a" ? "b" : "a";
})
</script> |
I looked at
I think this should behave the same as if it were connected, but I'm sure there are tricky details.
That one's less clear to me. (I didn't mean to give the impression that this would be easy, btw) |
Thanks for the workaround @emilio, I didn't see that get suggested in the stackoverflow post or anywhere else!
@jakearchibald I agree this sounds like it would be awesome for developers because it would just make iframes stop reloading as is. However, @rniwa and @smaug----'s words make me feel afraid of going forward with this due to the non-atomic nature of doing it async.
@sebmarkbage I totally agree, I think the best step for us to do right now is collect information on all the different cases where we are losing state due to reparenting, and how common/painful each one is. Does anyone have any thoughts on where it would be best for me to start compiling this list? Maybe a github gist or a google doc? And does anyone have any other cases to add or links to anything that would help assess how common/painful each case is? |
I think we need two things to make progress here:
|
@annevk Thanks for suggesting this path forward! I put the problematic use cases @sebmarkbage listed here: https://github.com/josepharhar/reparenting-loses-state @sebmarkbage are there any other related problematic use cases you know of? Are there any react issues you could point us to which show interest in any of these use cases? |
Happy to see that this is getting traction again! Just wanted to re-iterate the weight of the scenarios @sebmarkbage shared. The main issue we run into with working on Preact are the same. Loosing transition state or input state (focus/cursor position/text selection) are easily at the top of the most frequent issues we have with working with the DOM. So far we haven't found more issues than those already mentioned here. |
This wouldn't work for me as Firefox doesn't allow for hiding of the address bar (not even in popups from trusted sites) and I don't want to make real windows in the real OS but just simulated windows inside the page. And to the thing with svelte: |
we doing the exact same thing. i switched to svelte, because every component is like this:
this stackoverflow coping was important to me to. and the clean code of course. you can go with vanilla also, but svelte will even transfer your copied typescript or whatever snippets into vanilla automatically. plus the svelte environment from vite@latest is just devs dream. i shit you not :D Just copy/paste your vanilla and stackoverflow stuff and see for yourself. you will love it |
I tried a lot of frameworks and cms libs and services, but came to the conclusion that the JAMstack is my best new friend, since i want it clean vanilla, small, fast, edge-ready and re-usable (community friendly) which the svelte compiler/vite bundler in vite@latest comes with, right out of the box 😍😍😍 (npm build makes vanilla) 😍😍😍 so I can highly suggest it. and if I couldn't convince you of the svelte, minimum go for jamstack. https://jamstack.wtf/ both supports exactly what you are doing now, just better. ;) |
@worldpeaceenginelabs Your last few messages don't really seem on topic for an issue in the HTML repo like this. Let's keep the discussion here focused on reparenting iframes and discuss other things somewhere else. |
@jryans Hi :) There is no DOM in svelte. For your reparenting iframes issue it means, no dom update neccesary. Svelte advertises itself as truly reactive and its true. Svelte apps update itself partially. (in this case updating the frame, but not reloading the content) |
@worldpeaceenginelabs You can't convince me to use svelte. I'll try to look into other simulation's code to figure out an alternative solution. |
We support several screen elements that need to be aligned together: maximized elements on top-left (like video/screenshare/plugins), top-right elements (voice and text chats) and previews on bottom-right (video/plugins/screencast). Video/screencast/plugins can also move between top-left and bottom-right - the important thing here is to not make them detach from parent when moving between screen sections - and it's pretty challenging to satisfy all this. Current solution uses flexbox for SectionedScreen component and SectionedScreenPortal elements (used as Portal for all abovementioned). SectionedScreenPortal appends a child to SectionedScreen using proper class based on target section. Different sections' classes have slightly different styles/order and additional logic is applied to force the flexbox break into 2 rows if both top-right and bottom-right elements are detected. We use ":has" selector that currently has relatively limited support - it currently works on Chrome/Brave, but on Firefox you still need to activate it with config. There's a fallback option allowing almost same functionality without the ability to use full height of the screen when only top-left and/or top-right elements are present - they're at 60% height then. Using flexbox is a working though not perfect solution as it's not really intended for such layout. I tried display:grid but it becomes even more complex to manipulate it in flexible enough manner based on content. My original approach was with 2 nested containers - one for top/bottom row and one left/right for each row - it was working nicely as a layout but moving between sections triggers child detach, which causes interruptions with iframe videos, etc. It's probably possibly to use shadow DOM for translating screen element into different sections but I doubt it's possible using only css. Another planned approach was having React Context used by SectionedScreen and SectionedScreenPortals for sharing info between them and applying different styles and helper elements based on that, but it seems css with :has selector allows to satisfy all our current requirements. Some relatively new css selectors like :only-child, :only-of-type, :nth-of-type could also be of use in future. Some useful links: https://ishadeed.com/article/conditional-css/ https://tobiasahlin.com/blog/flexbox-break-to-new-row/ https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Flexible_Box_Layout/Aligning_Items_in_a_Flex_Container https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Grid_Layout/Basic_Concepts_of_Grid_Layout Shadow DOM: whatwg/html#5484 (comment) - nice and simple example https://stackoverflow.com/questions/42274721/shadow-dom-and-reactjs https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow
We support several screen elements that need to be aligned together: maximized elements on top-left (like video/screenshare/plugins), top-right elements (voice and text chats) and previews on bottom-right (video/plugins/screencast). Video/screencast/plugins can also move between top-left and bottom-right - the important thing here is to not make them detach from parent when moving between screen sections - and it's pretty challenging to satisfy all this. Current solution uses flexbox for SectionedScreen component and SectionedScreenPortal elements (used as Portal for all abovementioned). SectionedScreenPortal appends a child to SectionedScreen using proper class based on target section. Different sections' classes have slightly different styles/order and additional logic is applied to force the flexbox break into 2 rows if both top-right and bottom-right elements are detected. We use ":has" selector that currently has relatively limited support - it currently works on Chrome/Brave, but on Firefox you still need to activate it with config. There's a fallback option allowing almost same functionality without the ability to use full height of the screen when only top-left and/or top-right elements are present - they're at 60% height then. Using flexbox is a working though not perfect solution as it's not really intended for such layout. I tried display:grid but it becomes even more complex to manipulate it in flexible enough manner based on content. My original approach was with 2 nested containers - one for top/bottom row and one left/right for each row - it was working nicely as a layout but moving between sections triggers child detach, which causes interruptions with iframe videos, etc. It's probably possibly to use shadow DOM for translating screen element into different sections but I doubt it's possible using only css. Another planned approach was having React Context used by SectionedScreen and SectionedScreenPortals for sharing info between them and applying different styles and helper elements based on that, but it seems css with :has selector allows to satisfy all our current requirements. Some relatively new css selectors like :only-child, :only-of-type, :nth-of-type could also be of use in future. Some useful links: https://ishadeed.com/article/conditional-css/ https://tobiasahlin.com/blog/flexbox-break-to-new-row/ https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Flexible_Box_Layout/Aligning_Items_in_a_Flex_Container https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Grid_Layout/Basic_Concepts_of_Grid_Layout Shadow DOM: whatwg/html#5484 (comment) - nice and simple example https://stackoverflow.com/questions/42274721/shadow-dom-and-reactjs https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow
We support several screen elements that need to be aligned together: maximized elements on top-left (like video/screenshare/plugins), top-right elements (voice and text chats) and previews on bottom-right (video/plugins/screencast). Video/screencast/plugins can also move between top-left and bottom-right - the important thing here is to not make them detach from parent when moving between screen sections - and it's pretty challenging to satisfy all this. Current solution uses flexbox for SectionedScreen component and SectionedScreenPortal elements (used as Portal for all abovementioned). SectionedScreenPortal appends a child to SectionedScreen using proper class based on target section. Different sections' classes have slightly different styles/order and additional logic is applied to force the flexbox break into 2 rows if both top-right and bottom-right elements are detected. We use ":has" selector that currently has relatively limited support - it currently works on Chrome/Brave, but on Firefox you still need to activate it with config. There's a fallback option allowing almost same functionality without the ability to use full height of the screen when only top-left and/or top-right elements are present - they're at 60% height then. Using flexbox is a working though not perfect solution as it's not really intended for such layout. I tried display:grid but it becomes even more complex to manipulate it in flexible enough manner based on content. My original approach was with 2 nested containers - one for top/bottom row and one left/right for each row - it was working nicely as a layout but moving between sections triggers child detach, which causes interruptions with iframe videos, etc. It's probably possibly to use shadow DOM for translating screen element into different sections but I doubt it's possible using only css. Another planned approach was having React Context used by SectionedScreen and SectionedScreenPortals for sharing info between them and applying different styles and helper elements based on that, but it seems css with :has selector allows to satisfy all our current requirements. Some relatively new css selectors like :only-child, :only-of-type, :nth-of-type could also be of use in future. Some useful links: https://ishadeed.com/article/conditional-css/ https://tobiasahlin.com/blog/flexbox-break-to-new-row/ https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Flexible_Box_Layout/Aligning_Items_in_a_Flex_Container https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Grid_Layout/Basic_Concepts_of_Grid_Layout Shadow DOM: whatwg/html#5484 (comment) - nice and simple example https://stackoverflow.com/questions/42274721/shadow-dom-and-reactjs https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow
I have to respectfully ask that something be done with how IFrames behave when moving an IFrame element within the same parent, this is intolerable. Like others which have commented I am making a desktop designed as a front-end for a NAS system, and like most "web desktops" it operates with fake (div based) windows. These fake windows look and behave like ordinary windows and have a "send to back" and "bring to front" glyph on its borders. The problem is, that whenever someone clicks on the "send to back" glyph, I have no choice but to remove the window's element from the parent (which is the "desktop" div in this case), and then re-insert it into the same parent at either the beginning or end of the child collection ( insertBefore() | appendChild() ). We have spent 4 years building our own compiler, IDE and RTL for this project. The project is written in a unique flavor of Object Pascal designed especially for web-development, and the compiler emits highly effective JavaScript. Being able to move an element at least within the same parent (read: no re-parenting of the element, just move it within the child collection where the element is presently located) is something I quite frankly find strange is still lacking in 2023. This has been requested for 17 years (!) on the Mozilla forums. I strongly urge you to reconsider your position on this. What I would suggest is something that would not require a lot of work, but can be done relatively easily with the code that is already in the browser core. My suggestion is as follows: Add methods for moving elements within the same parent, which means no re-parenting is needed, and thus does not cause issues with scripting or security. Candidates on top of my head would be:
(*) The above would not affect z-order, only the position of the element in a parent's child collection. It would not affect security, mutation events or any of the existing infrastructure. And it would save people from having to re-map the zorder for all child elements just to change display order of an item. Looking at various posts around the internet where people debate the IFrame problem, the above would solve the majority of cases I have seen. Kind regards |
Anyone found any solution excluding the slot element? |
@annevk What are the script running bits that happen after the remove before the insert? Could we eliminate these? Alternately since we know from the single API call that a node is being moved could we give this move semantics and not apply side effects from insertion/removal in the internal operation if the source / destination documents are the same? |
Legacy mutation events are synchronous. They can observe the difference very easily. Also, iframes have semantics run when inserted and removed from the live tree, and there's ways to almost-synchronously observe the resulting navigation when the child and parent frames cooperate. Just to name a couple. |
Even if we create a new API, e.g. whatwg/dom#891, we would still have to answer these questions. My hope is that there are reasonable answers for how these should behave.
How breaking would it be if your event observers ran after the frame was already parented to the new node? This would leave no observable state where the frame was disconnected. Admittedly this would be a pretty big change for synchronous legacy mutation events but per this intent to deprecate they are already fairly low usage - the cases where the difference in timing of the removal during an operation which is immediately readding the node would be some fraction of those cases.
I'd have to familiarize myself with all of these semantics, but I think it may be reasonable to skip most/all of them in the case where it is known to remain in the document. |
@flackr Thanks for the call out there on usage. I'll take that bit back.
To be fair, iframes are literally mentioned in the title of this issue. Animations are another example, though in theory that could be worked around through some very hacky CSS (read: |
Right, but what I'm suggesting is that these side effects shouldn't happen when the element is known to be moving to a different position in the same document. It would make sense that animations shouldn't be canceled and restart as well (as long as the style in the new DOM location still resulted in the animation). |
I'm glad this is still being looked at because it is vital to be able to use iframes for isolation yet move them as needed, for workflows where you transition between two different views but need the content to be unaffected. In particular full screen views vs regular views, and other cases. |
What about instead of reparenting or moving iframes etc, we allow the slot element or something like slots to work not only in shadow root but everywhere? |
I have heard several requests for the ability to reparent iframes without reloading, including:
Has this been brought up as a spec issue before? I imagine someone must have thought about this before...
Would it make sense to add a method to iframes to reparent them to another element without reloading?
Another potential use case I've heard from React is to reparent elements other than iframes without losing state. An example of this would be to reparent an input element without losing focus and selection. In this case, perhaps we could have a reparenting method for any element?
cc @mfreed7 @tkent-google @sebmarkbage
The text was updated successfully, but these errors were encountered: