-
Notifications
You must be signed in to change notification settings - Fork 294
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
Introduce moveBefore()
state-preserving atomic move API
#1307
base: main
Are you sure you want to change the base?
Conversation
moveBefore()\
state-preserving atomic move APImoveBefore()
state-preserving atomic move API
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the mutation record needs some more design work. I would expect it to capture the information of a remove and an insert at the same time. Perhaps it needs to be a new object, though we could further overload the existing MutationRecord
as well I guess. At least I think you need:
- old target
- target
- moved node (I'm not sure you can ever move multiple at this point, but maybe we should allow for it in the mutation record design?)
- old previous sibling
- old next sibling
- previous sibling
- next sibling
Would be good to know what @smaug---- thinks and maybe @ajklein even wants to chime in.
dom.bs
Outdated
<li><p>Let <var>return node</var> be the result of <a>pre-inserting</a> <var>node</var> into | ||
<a>this</a> before <var>child</var>.</p></li> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be cleaner if we introduced "move" instead of overloading "insert", though I'm willing to be convinced. At least I always viewed this as introducing "move" as the third primitive following "insert" and "remove".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I think initially I saw it as a separate primitive too, but the more I looked at it, the more the difference between the two seemed really subtle. It mostly has to do with MutationObservers (and half of the relevant logic here is tucked away in the "remove" primitive) and not running the post-connection steps. So I feel like we'd end up with a near line-by-line copy of "insert", modulo one or two small differences. I'll take another look to see if my intuition is accurate, but I do kinda suspect this is where we'd end up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It'd be good to have more details. Because you can't move a DocumentFragment
so a vast number of checks don't apply. Adopt won't ever run.
There might be a number of range and shadow tree checks that end we end up duplicating, but perhaps that calls for abstracting those instead. At least to me a state flag seems rather unappealing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's possible there is enough from "insert" that we'd omit in "move", to make it creation worth it, sure.
At least to me a state flag seems rather unappealing.
With a separate "move" primitive and the decision to throw when we can't "move", we could get rid of the state variable that's currently in "insert" (i.e., <var>statePreservingAtomicMoveInProgress</var>
). But the state bool on Document
is what other specifications will refer to in their removal steps to react to a move appropriately, so I'm not sure we can get rid of that one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, wouldn't we give other specifications "move steps"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought about this a bit more. I think we're happy to create a new "move" primitive algorithm following the "insert" and "remove" algorithms. As you mentioned, it will be a simpler algorithm than "insert" since we'll avoid the DocumentFragment
stuff — oh and also the post-connection stuff, among maybe more things.
There's still the discussion around introducing either (a) new "move" steps/hook that other specs provide to react to moves, vs (b) a Document-scoped boolean that the new "move" algorithm sets, for existing insertion/removal steps to use, to not reset certain state inside those steps.
Option (b) is the direction of the current PR, which we think makes sense after implementing this in Chromium. I think this is because so far we've found no behavior that we specifically want to do during a move that isn't done by insertion/removal — so any "move steps" we provided would just be empty. Does that make sense?
Now, I think the real question here is whether we want to:
- Keep running the insertion/removal steps during a "move" operation, and just pepper
if during atomic move is true, don't run some of these substeps!
wherever state would ordinarily be reset- Cancel out the iframe removal steps
- Cancel img relevant mutations, to avoid re-firing load events
- Simply not run insertion/removal hooks at all during a move. This is only feasible if we determine that there are absolutely no steps in any spec's existing insertion/removal hooks that need to run during a move. We'll have to do an audit to determine this, which I'm starting now.
So the way I see it, those are probably our options, as opposed to creating new move steps which I think from our findings so far, would be unused. (We'll course-correct if this finding changes).
Shouldn't the target node be all the time the same, it is just the siblings which change. If this is really just remove and add back elsewhere, we could just reuse the existing childList MutationRecords, one for remove, one for adding node back, and possibly just add a flag to MutationRecord that it was about move. (movedNodes is a bit confusing, since it seems to depend on the connectedness of the relevant nodes and it is apparently empty for the removal part. And it is unclear to me why we need the connectedness check. This is about basic DOM tree operations, and I'd assume those to work the same way whether or not the node is connected) |
Creating two separate mutation records that a consumer would have to merge to (fully) understand it's a move seems suboptimal? I agree that it should probably work for disconnected nodes as well, but I don't think we want to support a case where the shadow-including root changes. |
It's been a long time since I've thought about this stuff, but I'm inclined to agree with @smaug---- that creating a new type of |
This CL makes moveBefore() match the spec PR [1], with regard to the agreed-upon error-throwing behavior for all pre-moving validity and hierarchy conditions. This means throwing an exception for: - Disconnected parent destination or move target - Cross-document Nodes - Destination parent that is not an Element node - Move target that is not an Element or character data [1]: whatwg/dom#1307 [email protected] Bug: 40150299 Change-Id: Iaf5243fb2762e21ede068a222600bd158859fe92 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5935350 Reviewed-by: Noam Rosenthal <[email protected]> Commit-Queue: Dominic Farolino <[email protected]> Cr-Commit-Position: refs/heads/main@{#1369303}
This CL makes moveBefore() match the spec PR [1], with regard to the agreed-upon error-throwing behavior for all pre-moving validity and hierarchy conditions. This means throwing an exception for: - Disconnected parent destination or move target - Cross-document Nodes - Destination parent that is not an Element node - Move target that is not an Element or character data [1]: whatwg/dom#1307 [email protected] Bug: 40150299 Change-Id: Iaf5243fb2762e21ede068a222600bd158859fe92 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5935350 Reviewed-by: Noam Rosenthal <[email protected]> Commit-Queue: Dominic Farolino <[email protected]> Cr-Commit-Position: refs/heads/main@{#1369303} Co-authored-by: Dominic Farolino <[email protected]>
…-move validity checks, a=testonly Automatic update from web-platform-tests DOM: Make moveBefore() throw for all pre-move validity checks (#48642) This CL makes moveBefore() match the spec PR [1], with regard to the agreed-upon error-throwing behavior for all pre-moving validity and hierarchy conditions. This means throwing an exception for: - Disconnected parent destination or move target - Cross-document Nodes - Destination parent that is not an Element node - Move target that is not an Element or character data [1]: whatwg/dom#1307 [email protected] Bug: 40150299 Change-Id: Iaf5243fb2762e21ede068a222600bd158859fe92 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5935350 Reviewed-by: Noam Rosenthal <[email protected]> Commit-Queue: Dominic Farolino <[email protected]> Cr-Commit-Position: refs/heads/main@{#1369303} Co-authored-by: Dominic Farolino <[email protected]> -- wpt-commits: 06b6ff6579327f235fff80b0f8bae8b0bbbc1069 wpt-pr: 48642
…-move validity checks, a=testonly Automatic update from web-platform-tests DOM: Make moveBefore() throw for all pre-move validity checks (#48642) This CL makes moveBefore() match the spec PR [1], with regard to the agreed-upon error-throwing behavior for all pre-moving validity and hierarchy conditions. This means throwing an exception for: - Disconnected parent destination or move target - Cross-document Nodes - Destination parent that is not an Element node - Move target that is not an Element or character data [1]: whatwg/dom#1307 [email protected] Bug: 40150299 Change-Id: Iaf5243fb2762e21ede068a222600bd158859fe92 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5935350 Reviewed-by: Noam Rosenthal <[email protected]> Commit-Queue: Dominic Farolino <[email protected]> Cr-Commit-Position: refs/heads/main@{#1369303} Co-authored-by: Dominic Farolino <[email protected]> -- wpt-commits: 06b6ff6579327f235fff80b0f8bae8b0bbbc1069 wpt-pr: 48642
|
||
<ol> | ||
<li><p>If either <var>parent</var> or <var>node</var> are not <a>connected</a>, then | ||
<a>throw</a> a "{{HierarchyRequestError!!exception}}" {{DOMException}}.</p></li> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, why do we have this limitation. Why not allow move within the same tree? Basically why the condition about shadow-including root isn't enough?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe we discussed this at TPAC. The idea is that moveBefore()
is used to preserve state, so in cases where state cannot be preserved, we should not just fall back to usual insertBefore()
-style behavior. Instead, we'll throw and let the developer handle this case themselves. Basically we let moveBefore()
succeed only when we're sure we can perform a state-preserving move, and when disconnected destinations or origins are involved, the state cannot be preserved.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But what state is there to preserve when you're in a disconnected subtree?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are three cases:
- If moving from connected -> disconnected, then there's lots of state to preserve, and you can't preserve any of it.
- If moving going from disconnected -> connected, then there's probably no state to preserve — I could see us wanting to enable this case. I don't feel strongly though!
- If going from disconnected -> disconnected, then there's also probably no state to preserve. I could also see us wanting to enable this case, but throwing and forcing developers to use
insertBefore()
seems fine to me too.
It seems simplest to treat all of the above cases the same, with the lowest common denominator being the first case above. I am sympathetic to allowing the last two cases though. @annevk What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would definitely expect a move operation to succeed if one stays within a same tree, whether or not your connected to document.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see how connected <> disconnected would work. It would need a completely different code path, no?
I can see connected <> connected and disconnected <> disconnected. Someone (either Noam or Dominic) argued against disconnected <> disconnected earlier, but it seems reasonable to allow as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm ok with both. One can say that disconnected<>disconnected doesn't have any state to preserve, so whether we throw or do the same as insertBefore is a judgement call.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should not do the same as insertBefore. It should run the moving steps (whether anyone cares about those for disconnected is a different question).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, I mean that it should be observably the same in the disconnected<>disconnected case.
need to update <a>live range</a> state when their <a for=range>start node</a> or | ||
<a for=range>end node</a> is an <a>inclusive descendant</a> of the <var>node</var>. This is | ||
because said <a>nodes</a> do not get removed from their <a>tree</a>, so ranges associated with | ||
them stay intact.</p> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But what if the move moves end node to be before the start node. That would leave the Range to an odd state, no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think so. It should be equivalent to when a range's start container is set (to the first time) to a node after the end container as well. Here are tests for this, which include intersectsNode
assertions to make sure the Range's state is not peculiar: https://github.com/web-platform-tests/wpt/blob/master/dom/nodes/moveBefore/tentative/live-range-updates.html.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, so something else still updates Range boundary points so that start is before end point? The note is then a bit confusing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, so something else still updates Range boundary points so that start is before end point?
No, a Range's start does not have to be before its end, in DOM tree order. Just like if you create a new Range whose start comes after its end in DOM tree order, the start's position will be "after" its end, which is observable via intersectsNode()
since nodes that are after end but before start, will no longer intersectsNode()
the Range. The same goes for when moveBefore()
moves things around such that the range's start is after its end, in tree order.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wait, how do you create a Range which has its start after end? setStart/End etc will modify the other boundary point if the order isn't right.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@smaug---- perhaps a useful middle ground can be that we preserve the selection only if the moved node is an ancestor of the entire range. This way if the range is moved while "intact", the selection would be preserved, but if parts of it are moved it's considered "broken" and it's dissolved. This is useful when moving whole elements which might have bits of text selected inside them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure what you mean with "wouldn't be able to preserve selection". Using the normal removal/addition rules would preserve selection the usual way. The range just gets modified.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean that it wouldn't be able to "preserve" selection at the previous start and end containers. Ordinarily, we can't preserve selection anchored at those containers, since those containers get removed from the DOM. But with moveBefore()
, those containers never leave the DOM, so we have an opportunity to do something better by keeping ranges anchored at those containers, which is a higher-fidelity preservation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Container approach (not modifying the range if full contained) would be still problematic if the container was moved from light dom to shadow dom, and the range was a range which one can access from selection.getRangeAt(). Selection should not reveal nodes in shadow dom.
Also it might be a bit surprising if selection is preserved in some cases but not in others.
Maybe, unrelated to moveBefore, there should be some helper methods to create a StaticRange from Range, and then update selection later, if/when needed, using that StaticRange. Hmm, though StaticRange isn't quite enough, since one needs also direction.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm OK with having the constraint of breaking the selection if moved between tree scopes
This CL enables moveBefore() inside connected ShadowRoot DocumentFragments, as the general only-Element-node target parent pre-condition was too strict, and disabled this case, despite it being a pretty valid one. This was discussed in whatwg/dom#1307 (comment). [email protected] Bug: 40150299 Change-Id: Ie9c6e31400b75b2b5d10d095c059012056a440da
This CL enables moveBefore() inside connected ShadowRoot DocumentFragments, as the general only-Element-node target parent pre-condition was too strict, and disabled this case, despite it being a pretty valid one. This was discussed in whatwg/dom#1307 (comment). [email protected] Bug: 40150299 Change-Id: Ie9c6e31400b75b2b5d10d095c059012056a440da Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6012161 Commit-Queue: Dominic Farolino <[email protected]> Reviewed-by: Noam Rosenthal <[email protected]> Cr-Commit-Position: refs/heads/main@{#1381208}
This CL enables moveBefore() inside connected ShadowRoot DocumentFragments, as the general only-Element-node target parent pre-condition was too strict, and disabled this case, despite it being a pretty valid one. This was discussed in whatwg/dom#1307 (comment). [email protected] Bug: 40150299 Change-Id: Ie9c6e31400b75b2b5d10d095c059012056a440da Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6012161 Commit-Queue: Dominic Farolino <[email protected]> Reviewed-by: Noam Rosenthal <[email protected]> Cr-Commit-Position: refs/heads/main@{#1381208}
This CL enables moveBefore() inside connected ShadowRoot DocumentFragments, as the general only-Element-node target parent pre-condition was too strict, and disabled this case, despite it being a pretty valid one. This was discussed in whatwg/dom#1307 (comment). [email protected] Bug: 40150299 Change-Id: Ie9c6e31400b75b2b5d10d095c059012056a440da Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6012161 Commit-Queue: Dominic Farolino <[email protected]> Reviewed-by: Noam Rosenthal <[email protected]> Cr-Commit-Position: refs/heads/main@{#1381208}
…ot DocumentFragments, a=testonly Automatic update from web-platform-tests DOM: Enable moveBefore() inside ShadowRoot DocumentFragments This CL enables moveBefore() inside connected ShadowRoot DocumentFragments, as the general only-Element-node target parent pre-condition was too strict, and disabled this case, despite it being a pretty valid one. This was discussed in whatwg/dom#1307 (comment). [email protected] Bug: 40150299 Change-Id: Ie9c6e31400b75b2b5d10d095c059012056a440da Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6012161 Commit-Queue: Dominic Farolino <[email protected]> Reviewed-by: Noam Rosenthal <[email protected]> Cr-Commit-Position: refs/heads/main@{#1381208} -- wpt-commits: f54cd920024da0857fdc5b036f16a7d1fd8792fd wpt-pr: 49100
…ot DocumentFragments, a=testonly Automatic update from web-platform-tests DOM: Enable moveBefore() inside ShadowRoot DocumentFragments This CL enables moveBefore() inside connected ShadowRoot DocumentFragments, as the general only-Element-node target parent pre-condition was too strict, and disabled this case, despite it being a pretty valid one. This was discussed in whatwg/dom#1307 (comment). [email protected] Bug: 40150299 Change-Id: Ie9c6e31400b75b2b5d10d095c059012056a440da Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6012161 Commit-Queue: Dominic Farolino <[email protected]> Reviewed-by: Noam Rosenthal <[email protected]> Cr-Commit-Position: refs/heads/main@{#1381208} -- wpt-commits: f54cd920024da0857fdc5b036f16a7d1fd8792fd wpt-pr: 49100
…ot DocumentFragments, a=testonly Automatic update from web-platform-tests DOM: Enable moveBefore() inside ShadowRoot DocumentFragments This CL enables moveBefore() inside connected ShadowRoot DocumentFragments, as the general only-Element-node target parent pre-condition was too strict, and disabled this case, despite it being a pretty valid one. This was discussed in whatwg/dom#1307 (comment). R=nrosenthalchromium.org Bug: 40150299 Change-Id: Ie9c6e31400b75b2b5d10d095c059012056a440da Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6012161 Commit-Queue: Dominic Farolino <domchromium.org> Reviewed-by: Noam Rosenthal <nrosenthalchromium.org> Cr-Commit-Position: refs/heads/main{#1381208} -- wpt-commits: f54cd920024da0857fdc5b036f16a7d1fd8792fd wpt-pr: 49100 UltraBlame original commit: 05e7de975711d7be9749fcd35b503aed6225b2b7
…ot DocumentFragments, a=testonly Automatic update from web-platform-tests DOM: Enable moveBefore() inside ShadowRoot DocumentFragments This CL enables moveBefore() inside connected ShadowRoot DocumentFragments, as the general only-Element-node target parent pre-condition was too strict, and disabled this case, despite it being a pretty valid one. This was discussed in whatwg/dom#1307 (comment). R=nrosenthalchromium.org Bug: 40150299 Change-Id: Ie9c6e31400b75b2b5d10d095c059012056a440da Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6012161 Commit-Queue: Dominic Farolino <domchromium.org> Reviewed-by: Noam Rosenthal <nrosenthalchromium.org> Cr-Commit-Position: refs/heads/main{#1381208} -- wpt-commits: f54cd920024da0857fdc5b036f16a7d1fd8792fd wpt-pr: 49100 UltraBlame original commit: 05e7de975711d7be9749fcd35b503aed6225b2b7
…ot DocumentFragments, a=testonly Automatic update from web-platform-tests DOM: Enable moveBefore() inside ShadowRoot DocumentFragments This CL enables moveBefore() inside connected ShadowRoot DocumentFragments, as the general only-Element-node target parent pre-condition was too strict, and disabled this case, despite it being a pretty valid one. This was discussed in whatwg/dom#1307 (comment). R=nrosenthalchromium.org Bug: 40150299 Change-Id: Ie9c6e31400b75b2b5d10d095c059012056a440da Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6012161 Commit-Queue: Dominic Farolino <domchromium.org> Reviewed-by: Noam Rosenthal <nrosenthalchromium.org> Cr-Commit-Position: refs/heads/main{#1381208} -- wpt-commits: f54cd920024da0857fdc5b036f16a7d1fd8792fd wpt-pr: 49100 UltraBlame original commit: 05e7de975711d7be9749fcd35b503aed6225b2b7
This PR introduces a new DOM API on the
Node
interface:moveBefore()
. It mirrorsinsertBefore()
in shape, but defers to a new DOM manipulation primitive that this PR adds in service of this new API: the "move" primitive. The move primitive contains some of the DOM tree bookkeeping steps from the remove primitive, as well as the insert primitive, and does three more interesting things:connectedMoveCallback()
The power of the move primitive comes from the fact that the algorithm does not defer to the traditional insert and removal primitives, and therefore does not invoke the removing steps and insertion steps. This allows most state to be preserved by default (i.e., we don't tear down iframes, or close dialogs). Sometimes, the insertion/removing step overrides in other specifications have steps that do need to be performed during a move anyways. These specifications are expected to override the moving steps hook and perform the necessary work accordingly. See whatwg/html#10657 for HTML.
Remaining tasks (some will be PRs in other standards):
focusin
event semantics[ ] Preserve text-selection. See set the selection range. Edit: Nothing needs to be done here. Selection metadata (i.e.,selectionStart
and kin) is preserved by default in browsers, consistent with HTML (no action is taken on removal). The UI behavior of the selection not being highlighted is a side-effect of the element losing focusselectionchange
event: We've decided to allowselectionchange
event to still fire, since it is queued in a task. No changes for this part are required.Node.prototype.moveBefore
) WebKit/standards-positions#375Node.prototype.moveBefore
) mozilla/standards-positions#1053(See WHATWG Working Mode: Changes for more details.)
Preview | Diff